diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index c317cb47ad..029b436178 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -20,7 +20,10 @@ enum CustomerCenterConfigTestData { @available(iOS 14.0, *) // swiftlint:disable:next function_body_length - static func customerCenterData(lastPublishedAppVersion: String?) -> CustomerCenterConfigData { + static func customerCenterData( + lastPublishedAppVersion: String?, + shouldWarnCustomerToUpdate: Bool = false + ) -> CustomerCenterConfigData { CustomerCenterConfigData( screens: [.management: .init( @@ -111,7 +114,10 @@ enum CustomerCenterConfigTestData { "back": "Back" ] ), - support: .init(email: "test-support@revenuecat.com"), + support: .init( + email: "test-support@revenuecat.com", + shouldWarnCustomerToUpdate: shouldWarnCustomerToUpdate + ), lastPublishedAppVersion: lastPublishedAppVersion, productId: 1 ) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 6654919eaa..21b6c6b32e 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -35,6 +35,14 @@ import RevenueCat private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion private(set) var purchasesProvider: CustomerCenterPurchasesType + @Published + private(set) var onUpdateAppClick: (() -> Void)? + + /// Whether or not the Customer Center should warn the customer that they're on an outdated version of the app. + var shouldShowAppUpdateWarnings: Bool { + return !appIsLatestVersion && (configuration?.support.shouldWarnCustomerToUpdate ?? true) + } + // @PublicForExternalTesting @Published var state: CustomerCenterViewState { @@ -130,6 +138,13 @@ import RevenueCat func loadCustomerCenterConfig() async { do { self.configuration = try await Purchases.shared.loadCustomerCenter() + if let productId = configuration?.productId { + self.onUpdateAppClick = { + // productId is a positive integer, so it should be safe to construct a URL from it. + let url = URL(string: "https://itunes.apple.com/app/id\(productId)")! + URLUtilities.openURLIfNotAppExtension(url) + } + } } catch { self.state = .error(error) } diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 4d958b853a..0a4637ba6e 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -29,17 +29,6 @@ struct AppUpdateWarningView: View { self.onContinueAnywayClick = onContinueAnywayClick } - init(productId: UInt, onContinueAnywayClick: @escaping () -> Void) { - self.init( - onUpdateAppClick: { - // productId is a positive integer, so it should be safe to construct a URL from it. - let url = URL(string: "https://itunes.apple.com/app/id\(productId)")! - URLUtilities.openURLIfNotAppExtension(url) - }, - onContinueAnywayClick: onContinueAnywayClick - ) - } - @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization @Environment(\.appearance) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index c67fc2eed4..e1de03b60c 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -115,9 +115,10 @@ private extension CustomerCenterView { if let purchaseInformation = viewModel.purchaseInformation { if purchaseInformation.store == .appStore, let screen = configuration.screens[.management] { - if let productId = configuration.productId, !ignoreAppUpdateWarning && !viewModel.appIsLatestVersion { + if let onUpdateAppClick = viewModel.onUpdateAppClick, + !ignoreAppUpdateWarning && viewModel.shouldShowAppUpdateWarnings { AppUpdateWarningView( - productId: productId, + onUpdateAppClick: onUpdateAppClick, onContinueAnywayClick: { withAnimation { ignoreAppUpdateWarning = true diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index f88ef75bf0..fdc41d0a87 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -57,56 +57,117 @@ struct RestorePurchasesAlert: ViewModifier { func body(content: Content) -> some View { content - .alert(isPresented: $isPresented) { - switch self.alertType { - case .restorePurchases: - return Alert( - title: Text(localization.commonLocalizedString(for: .restorePurchases)), - message: Text(localization.commonLocalizedString(for: .goingToCheckPurchases)), - primaryButton: .default(Text(localization.commonLocalizedString(for: .checkPastPurchases)), - action: { - Task { - let alertType = - await self.customerCenterViewModel.performRestore() - self.setAlertType(alertType) - } - }), - secondaryButton: .cancel(Text(localization.commonLocalizedString(for: .cancel))) - ) - - case .purchasesRecovered: - return Alert(title: Text(localization.commonLocalizedString(for: .purchasesRecovered)), - message: Text(localization.commonLocalizedString(for: .purchasesRecoveredExplanation)), - dismissButton: .cancel(Text(localization.commonLocalizedString(for: .dismiss))) { - dismiss() - }) - - case .purchasesNotFound: - let message = Text(localization.commonLocalizedString(for: .purchasesNotRecovered)) - if let url = supportURL { - return Alert(title: Text(""), - message: message, - primaryButton: .default( - Text(localization.commonLocalizedString(for: .contactSupport)) - ) { - Task { - openURL(url) - } - }, - secondaryButton: .cancel(Text(localization.commonLocalizedString(for: .dismiss))) { - dismiss() - }) - } else { - return Alert(title: Text(""), - message: message, - dismissButton: .default(Text(localization.commonLocalizedString(for: .dismiss))) { - dismiss() - }) + .confirmationDialog( + alertTitle(), + isPresented: $isPresented, + actions: { + switch alertType { + case .purchasesRecovered: + PurchasesRecoveredActions() + case .purchasesNotFound: + PurchasesNotFoundActions() + case .restorePurchases: + RestorePurchasesActions() } + }, + message: { + Text(alertMessage()) } + ) + } + + // MARK: - Actions + @ViewBuilder + // swiftlint:disable:next identifier_name + private func RestorePurchasesActions() -> some View { + Button { + Task { + let alertType = await self.customerCenterViewModel.performRestore() + self.setAlertType(alertType) + } + } label: { + Text(localization.commonLocalizedString(for: .checkPastPurchases)) + } + + Button(role: .cancel) { + dismissAlert() + } label: { + Text(localization.commonLocalizedString(for: .cancel)) + } + } + + @ViewBuilder + // swiftlint:disable:next identifier_name + private func PurchasesRecoveredActions() -> some View { + Button(role: .cancel) { + dismissAlert() + } label: { + Text(localization.commonLocalizedString(for: .dismiss)) + } + } + + @ViewBuilder + // swiftlint:disable:next identifier_name + private func PurchasesNotFoundActions() -> some View { + + if let onUpdateAppClick = customerCenterViewModel.onUpdateAppClick, + customerCenterViewModel.shouldShowAppUpdateWarnings { + Button { + onUpdateAppClick() + } label: { + Text(localization.commonLocalizedString(for: .updateWarningUpdate)) + .bold() + } + } + + if let url = supportURL { + Button { + Task { + openURL(url) + } + } label: { + Text(localization.commonLocalizedString(for: .contactSupport)) } + } + + Button(role: .cancel) { + dismissAlert() + } label: { + Text(localization.commonLocalizedString(for: .dismiss)) + } } + // MARK: - Strings + private func alertTitle() -> String { + switch self.alertType { + case .purchasesRecovered: + return localization.commonLocalizedString(for: .purchasesRecovered) + case .purchasesNotFound: + return "" + case .restorePurchases: + return localization.commonLocalizedString(for: .restorePurchases) + } + } + + private func alertMessage() -> String { + switch self.alertType { + case .purchasesRecovered: + return localization.commonLocalizedString(for: .purchasesRecoveredExplanation) + case .purchasesNotFound: + var message = localization.commonLocalizedString(for: .purchasesNotRecovered) + if customerCenterViewModel.shouldShowAppUpdateWarnings { + message += "\n\n" + localization.commonLocalizedString(for: .updateWarningDescription) + } + return message + case .restorePurchases: + return localization.commonLocalizedString(for: .goingToCheckPurchases) + } + } + + private func dismissAlert() { + self.alertType = .restorePurchases + dismiss() + } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 38780eaa91..093c84e5df 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -32,6 +32,9 @@ struct WrongPlatformView: View { @State private var purchaseInformation: PurchaseInformation + @EnvironmentObject + private var customerCenterViewModel: CustomerCenterViewModel + private let screen: CustomerCenterConfigData.Screen? @Environment(\.localization) diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 70193af705..48aa3cc490 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -412,9 +412,14 @@ public struct CustomerCenterConfigData { public struct Support { public let email: String + public let shouldWarnCustomerToUpdate: Bool - public init(email: String) { + public init( + email: String, + shouldWarnCustomerToUpdate: Bool + ) { self.email = email + self.shouldWarnCustomerToUpdate = shouldWarnCustomerToUpdate } } @@ -550,6 +555,7 @@ extension CustomerCenterConfigData.Support { init(from response: CustomerCenterConfigResponse.Support) { self.email = response.email + self.shouldWarnCustomerToUpdate = response.shouldWarnCustomerToUpdate ?? true } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 2748d169b9..6984bae308 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -132,6 +132,7 @@ struct CustomerCenterConfigResponse { struct Support { let email: String + let shouldWarnCustomerToUpdate: Bool? } diff --git a/Tests/RevenueCatUITests/CustomerCenter/ContactSupportUtilitiesTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ContactSupportUtilitiesTests.swift index 44f8bff4cd..21e993a56a 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ContactSupportUtilitiesTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ContactSupportUtilitiesTests.swift @@ -18,7 +18,10 @@ import XCTest class ContactSupportUtilitiesTest: TestCase { - private let support: CustomerCenterConfigData.Support = .init(email: "support@example.com") + private let support: CustomerCenterConfigData.Support = .init( + email: "support@example.com", + shouldWarnCustomerToUpdate: false + ) private let localization: CustomerCenterConfigData.Localization = .init(locale: "en_US", localizedStrings: [:]) func testSupportEmailBodyWithDefaultDataIsCorrect() { diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 30f5c5d389..7323437211 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -22,6 +22,7 @@ import XCTest #if os(iOS) +// swiftlint:disable file_length @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -734,6 +735,55 @@ class CustomerCenterViewModelTests: TestCase { } } + func testShouldShowAppUpdateWarningsTrue() { + let mockPurchases = MockCustomerCenterPurchases() + let latestVersion = "3.0.0" + let currentVersion = "2.0.0" + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return currentVersion }, + purchasesProvider: mockPurchases + ) + viewModel.configuration = CustomerCenterConfigTestData.customerCenterData( + lastPublishedAppVersion: latestVersion, + shouldWarnCustomerToUpdate: true + ) + + expect(viewModel.shouldShowAppUpdateWarnings).to(beTrue()) + } + + func testShouldShowAppUpdateWarningsFalse() { + let mockPurchases = MockCustomerCenterPurchases() + let latestVersion = "3.0.0" + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return latestVersion }, + purchasesProvider: mockPurchases + ) + viewModel.configuration = CustomerCenterConfigTestData.customerCenterData( + lastPublishedAppVersion: latestVersion, + shouldWarnCustomerToUpdate: true + ) + + expect(viewModel.shouldShowAppUpdateWarnings).to(beFalse()) + } + + func testShouldShowAppUpdateWarningsFalseIfBlockedByConfig() { + let mockPurchases = MockCustomerCenterPurchases() + let latestVersion = "3.0.0" + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return latestVersion }, + purchasesProvider: mockPurchases + ) + viewModel.configuration = CustomerCenterConfigTestData.customerCenterData( + lastPublishedAppVersion: latestVersion, + shouldWarnCustomerToUpdate: false + ) + + expect(viewModel.shouldShowAppUpdateWarnings).to(beFalse()) + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index 869eb5d90b..3e5863d1e1 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -106,7 +106,10 @@ class CustomerCenterConfigDataTests: TestCase { ) ], localization: .init(locale: "en_US", localizedStrings: ["key": "value"]), - support: .init(email: "support@example.com") + support: .init( + email: "support@example.com", + shouldWarnCustomerToUpdate: false + ) ), lastPublishedAppVersion: "1.2.3", itunesTrackId: 123 @@ -178,6 +181,8 @@ class CustomerCenterConfigDataTests: TestCase { expect(configData.lastPublishedAppVersion) == "1.2.3" expect(configData.productId) == 123 + + expect(configData.support.shouldWarnCustomerToUpdate) == false } func testUnknownValuesHandling() throws {