Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ButtonComponent can dismiss the paywall #4365

Merged
merged 39 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8324986
Adds scaffolding for the ButtonComponent.
JayShortway Oct 3, 2024
4200f5e
ButtonComponent uses the correct type.
JayShortway Oct 3, 2024
13c48c6
Adds an empty preview
JayShortway Oct 3, 2024
95fe157
Buttons contain text!
JayShortway Oct 4, 2024
0bfe862
First pass of the button Action enum, and some click handling.
JayShortway Oct 4, 2024
857fad6
Removes EmptyView.
JayShortway Oct 4, 2024
62b33f7
Buttons are stacks now
JayShortway Oct 4, 2024
310bee9
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 4, 2024
c388b8b
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 4, 2024
a5e4fa9
removes deeplink method
JayShortway Oct 4, 2024
bd8ae50
Removes oopsie from bad merge
JayShortway Oct 7, 2024
b29d263
Uses swiftlint:disable:next
JayShortway Oct 7, 2024
64a27d6
Adds deepLink URLMethod.
JayShortway Oct 7, 2024
93316d7
Adds privacyPolicy and terms cases to Destination enum.
JayShortway Oct 7, 2024
5aacf0d
Changes url properties to urlLid.
JayShortway Oct 7, 2024
26fbb79
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 7, 2024
0e93154
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 7, 2024
393590a
Makes ButtonComponentViewModel public for PaywallsTester
JayShortway Oct 7, 2024
c0f4f73
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 7, 2024
4f849b0
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 7, 2024
06e704b
Adds an error message.
JayShortway Oct 7, 2024
9f0e7a0
Adds URL parsing and a lot of switch statements.
JayShortway Oct 7, 2024
51e0d06
Handles URL destinations.
JayShortway Oct 8, 2024
0d0d15c
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 8, 2024
dd0f48c
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 8, 2024
26d9e8e
Merge branch 'paywalls-components/button-component-2' into paywalls-c…
JayShortway Oct 8, 2024
d5feec1
Deletes accidentally created file
JayShortway Oct 8, 2024
a0b1edf
Do not attempt to use an in-app browser on tvOS.
JayShortway Oct 8, 2024
de4d515
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 9, 2024
f38e654
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 9, 2024
102b36b
Merge branch 'paywalls-components/button-component-2' into paywalls-c…
JayShortway Oct 9, 2024
8448d51
Adds onDismiss closure param
JayShortway Oct 9, 2024
d900448
Actually calls onDismiss
JayShortway Oct 9, 2024
ce6c7f6
Fixes lint.
JayShortway Oct 10, 2024
c2774d7
Merge branch 'main' into paywalls-components/button-component-1
JayShortway Oct 11, 2024
5f28f27
Merge branch 'paywalls-components/button-component-1' into paywalls-c…
JayShortway Oct 11, 2024
6d219fa
Merge branch 'paywalls-components/button-component-2' into paywalls-c…
JayShortway Oct 11, 2024
1b04320
Merge branch 'paywalls-components/button-component-3' into paywalls-c…
JayShortway Oct 11, 2024
79369f7
Merge branch 'main' into paywalls-components/button-component-4
JayShortway Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@
57FFD2522922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; };
6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; };
7706ED3E2C6E374D0004B9F9 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */; };
7707A94C2CAD93AC006E0313 /* PaywallButtonComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7707A94B2CAD93AC006E0313 /* PaywallButtonComponent.swift */; };
7707A94E2CAD94D2006E0313 /* ButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7707A94D2CAD94D2006E0313 /* ButtonComponentViewModel.swift */; };
7707A9502CAD9775006E0313 /* ButtonComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7707A94F2CAD9775006E0313 /* ButtonComponentView.swift */; };
77372D992C6F8C7B008E59D3 /* AppUpdateWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */; };
77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */; };
777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; };
Expand Down Expand Up @@ -1820,6 +1823,9 @@
57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = "<group>"; };
7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = "<group>"; };
7707A93B2CAD8AA2006E0313 /* Local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Local.xcconfig; sourceTree = "<group>"; };
7707A94B2CAD93AC006E0313 /* PaywallButtonComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallButtonComponent.swift; sourceTree = "<group>"; };
7707A94D2CAD94D2006E0313 /* ButtonComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonComponentViewModel.swift; sourceTree = "<group>"; };
7707A94F2CAD9775006E0313 /* ButtonComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonComponentView.swift; sourceTree = "<group>"; };
77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateWarningView.swift; sourceTree = "<group>"; };
77607FD72CAD87D60066C23C /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = "<group>"; };
77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3914,6 +3920,15 @@
path = Security;
sourceTree = "<group>";
};
7707A94A2CAD936A006E0313 /* Button */ = {
isa = PBXGroup;
children = (
7707A94D2CAD94D2006E0313 /* ButtonComponentViewModel.swift */,
7707A94F2CAD9775006E0313 /* ButtonComponentView.swift */,
);
path = Button;
sourceTree = "<group>";
};
887A5FBB2C1D036200E1A461 /* RevenueCatUIDev */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4270,6 +4285,7 @@
isa = PBXGroup;
children = (
88AD01012C740CF400AA1F2B /* PaywallComponentBase.swift */,
7707A94B2CAD93AC006E0313 /* PaywallButtonComponent.swift */,
88EA80EE2C87D68F003E6675 /* PaywallComponentLocalization.swift */,
88B1BAD72C812D72001B7EE5 /* PaywallComponentPropertyTypes.swift */,
88AD01032C740CF400AA1F2B /* PaywallImageComponent.swift */,
Expand All @@ -4284,6 +4300,7 @@
88AD01352C74196600AA1F2B /* Components */ = {
isa = PBXGroup;
children = (
7707A94A2CAD936A006E0313 /* Button */,
2C2AEB0D2CA64DA900A50F38 /* Previews */,
88B1BAE62C813A3C001B7EE5 /* Image */,
88B1BAE22C813A3C001B7EE5 /* LinkButton */,
Expand Down Expand Up @@ -5512,6 +5529,7 @@
B32B74FF26868AEB005647BF /* Package.swift in Sources */,
353756522C382BC700A1B8D6 /* PreferredLocalesProvider.swift in Sources */,
35D159CF2BC43B89004D8061 /* DiagnosticsSynchronizer.swift in Sources */,
7707A94C2CAD93AC006E0313 /* PaywallButtonComponent.swift in Sources */,
578DAA482948EEAD001700FD /* Clock.swift in Sources */,
2DDF41B324F6F387005BC22D /* InAppPurchaseBuilder.swift in Sources */,
4F4FF3E12A3B731A0028018C /* ETagStrings.swift in Sources */,
Expand Down Expand Up @@ -5975,6 +5993,7 @@
887A60702C1D037000E1A461 /* PaywallTemplate.swift in Sources */,
887A60882C1D037000E1A461 /* MockPurchases.swift in Sources */,
887A60BC2C1D037000E1A461 /* Template5View.swift in Sources */,
7707A9502CAD9775006E0313 /* ButtonComponentView.swift in Sources */,
887A60BE2C1D037000E1A461 /* PaywallFooterViewController.swift in Sources */,
887A608A2C1D037000E1A461 /* PurchaseHandler.swift in Sources */,
2D2AFE8D2C6A834D00D1B0B4 /* TestData.swift in Sources */,
Expand Down Expand Up @@ -6065,6 +6084,7 @@
887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */,
887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */,
3537566A2C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift in Sources */,
7707A94E2CAD94D2006E0313 /* ButtonComponentViewModel.swift in Sources */,
887A60842C1D037000E1A461 /* ConsistentPackageContentView.swift in Sources */,
887A60C42C1D037000E1A461 /* IconView.swift in Sources */,
887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions RevenueCatUI/Data/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ enum Strings {
case paywall_view_model_construction_failed(Error)
case paywall_contains_no_localization_data
case paywall_could_not_find_localization(String)
case paywall_invalid_url(String)

// Customer Center
case could_not_find_subscription_information
Expand Down Expand Up @@ -165,6 +166,9 @@ extension Strings: CustomStringConvertible {
case .paywall_contains_no_localization_data:
return "Paywall contains no localization data."

case .paywall_invalid_url(let urlLid):
return "No valid URL is configured for \(urlLid)"

case .invalid_color_string(let colorString):
return "Invalid hex color string: \(colorString)"

Expand Down
12 changes: 11 additions & 1 deletion RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,17 @@ public struct PaywallView: View {

#if PAYWALL_COMPONENTS
if let componentData = offering.paywallComponentsData {
TemplateComponentsView(paywallComponentsData: componentData, offering: offering)
TemplateComponentsView(
paywallComponentsData: componentData,
offering: offering,
onDismiss: {
guard let onRequestedDismissal = self.onRequestedDismissal else {
self.dismiss()
return
}
onRequestedDismissal()
}
)
} else {

let (paywall, displayedLocale, template, error) = offering.validatedPaywall(locale: self.locale)
Expand Down
149 changes: 149 additions & 0 deletions RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// ButtonComponentView.swift
//
// Created by Jay Shortway on 02/10/2024.

import Foundation
import RevenueCat
#if canImport(SafariServices)
import SafariServices
#endif
import SwiftUI

#if PAYWALL_COMPONENTS

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct ButtonComponentView: View {
@Environment(\.openURL) var openURL
@State private var inAppBrowserURL: URL?

private let viewModel: ButtonComponentViewModel
private let onDismiss: () -> Void

internal init(viewModel: ButtonComponentViewModel, onDismiss: @escaping () -> Void) {
self.viewModel = viewModel
self.onDismiss = onDismiss
}

var body: some View {
Button(
action: { onClick() },
label: { StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) }
).sheet(isPresented: .isNotNil($inAppBrowserURL)) {
SafariView(url: inAppBrowserURL!)
}
}

private func onClick() {
switch viewModel.component.action {
case .restorePurchases:
// swiftlint:disable:next todo
// TODO handle restoring purchases
break
case .navigateTo(let destination):
navigateTo(destination: destination)
case .navigateBack:
onDismiss()
}
}

private func navigateTo(destination: PaywallComponent.ButtonComponent.Destination) {
switch destination {
case .customerCenter:
// swiftlint:disable:next todo
// TODO handle navigating to customer center
break
case .URL(let urlLid, let method),
.privacyPolicy(let urlLid, let method),
.terms(let urlLid, let method):
navigateToUrl(urlLid: urlLid, method: method)
}
}

private func navigateToUrl(urlLid: String, method: PaywallComponent.ButtonComponent.URLMethod) {
guard let urlString = try? viewModel.localizedStrings.string(key: urlLid),
let url = URL(string: urlString) else {
Logger.error(Strings.paywall_invalid_url(urlLid))
return
}

switch method {
case .inAppBrowser:
#if os(tvOS)
// There's no SafariServices on tvOS, so we're falling back to opening in an external browser.
openURL(url)
#else
inAppBrowserURL = url
#endif
case .externalBrowser,
.deepLink:
openURL(url)
}
}

}

private struct SafariView: UIViewControllerRepresentable {
let url: URL

func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
SFSafariViewController(url: url)
}

func updateUIViewController(
_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>
) {
// No updates needed
}
}

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct ButtonComponentView_Previews: PreviewProvider {

static var previews: some View {
VStack {
ButtonComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
component: .init(
action: .navigateBack,
stack: .init(
components: [
PaywallComponent.text(
PaywallComponent.TextComponent(
textLid: "buttonText",
color: .init(light: "#000000")
)
)
],
backgroundColor: nil
)
),
locale: Locale(identifier: "en_US"),
localizedStrings: [
"buttonText": PaywallComponentsData.LocalizationData.string("Do something")
],
offering: Offering(identifier: "", serverDescription: "", availablePackages: [])
),
onDismiss: { }
)
}
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Default")
}
}

#endif

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// ButtonComponentViewModel.swift
//
// Created by Jay Shortway on 02/10/2024.
//
// swiftlint:disable missing_docs

import Foundation
import RevenueCat

#if PAYWALL_COMPONENTS

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public class ButtonComponentViewModel {

internal let component: PaywallComponent.ButtonComponent
internal let localizedStrings: PaywallComponent.LocalizationDictionary
let stackViewModel: StackComponentViewModel

init(
component: PaywallComponent.ButtonComponent,
locale: Locale,
localizedStrings: PaywallComponent.LocalizationDictionary,
offering: Offering
) throws {
self.component = component
self.localizedStrings = localizedStrings
self.stackViewModel = try StackComponentViewModel(
locale: locale,
component: component.stack,
localizedStrings: localizedStrings,
offering: offering
)
}

}

#endif
10 changes: 10 additions & 0 deletions RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum PaywallComponentViewModel {
case spacer(SpacerComponentViewModel)
case stack(StackComponentViewModel)
case linkButton(LinkButtonComponentViewModel)
case button(ButtonComponentViewModel)

}

Expand Down Expand Up @@ -55,6 +56,15 @@ extension PaywallComponent {
try LinkButtonComponentViewModel(component: component,
localizedStrings: localizedStrings)
)
case .button(let component):
return .button(
try ButtonComponentViewModel(
component: component,
locale: locale,
localizedStrings: localizedStrings,
offering: offering
)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ struct Template1Preview_Previews: PreviewProvider {
paywallComponentsData: Template1Preview.data,
offering: .init(identifier: "",
serverDescription: "",
availablePackages: [])
availablePackages: []),
onDismiss: { }
)
.previewLayout(.fixed(width: 400, height: 800))
.previewDisplayName("Template 1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SwiftUI
struct StackComponentView: View {

let viewModel: StackComponentViewModel
let onDismiss: () -> Void

var body: some View {
Group {
Expand All @@ -28,17 +29,17 @@ struct StackComponentView: View {
// LazyVStack needed for performance when loading
LazyVStack(spacing: viewModel.spacing) {
Group {
ComponentsView(componentViewModels: self.viewModel.viewModels)
ComponentsView(componentViewModels: self.viewModel.viewModels, onDismiss: self.onDismiss)
}
.frame(maxWidth: .infinity, alignment: horizontalAlignment.stackAlignment)
}
case .horizontal(let verticalAlignment):
HStack(alignment: verticalAlignment.stackAlignment, spacing: viewModel.spacing) {
ComponentsView(componentViewModels: self.viewModel.viewModels)
ComponentsView(componentViewModels: self.viewModel.viewModels, onDismiss: self.onDismiss)
}
case .zlayer(let alignment):
ZStack(alignment: alignment.stackAlignment) {
ComponentsView(componentViewModels: self.viewModel.viewModels)
ComponentsView(componentViewModels: self.viewModel.viewModels, onDismiss: self.onDismiss)
}
}
}
Expand Down
Loading