From 444d450cafbbe4038f6d38ebe1ff926e2c31a9f2 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 17 Jun 2024 14:42:00 +0200 Subject: [PATCH 01/90] Create file --- .../CustomerCenter/Data/CustomerCenterConfigData.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift new file mode 100644 index 0000000000..a5fc721b20 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Cesar de la Vega on 17/6/24. +// + +import Foundation From c5aaf56a5f18ae4b0211f649a56559078638dc4a Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 28 Jun 2024 12:16:54 +0200 Subject: [PATCH 02/90] [Customer Center] Create `CustomerCenterView` (#3919) Base branch is `integration/customer_support_workflow` so we don't merge into `main` yet Borrows a lot from #3865 Creates a new `CustomerCenterView` that can be used as a customer support workflow starting point. All details can be found in https://linear.app/revenuecat/project/sdk-support-workflow-cf7f6a1d5340/overview --------- Co-authored-by: Will Taylor Co-authored-by: James Borthwick <109382862+jamesrb1@users.noreply.github.com> --- .vscode/settings.json | 4 + RevenueCat.xcodeproj/project.pbxproj | 16 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + .../Data/CustomerCenterConfigData.swift | 59 ++- .../Data/CustomerCenterConfigTestData.swift | 79 ++++ .../Data/CustomerCenterError.swift | 41 ++ .../Data/SubscriptionInformation.swift | 50 +++ .../ManageSubscriptionsButtonStyle.swift | 50 +++ .../ManageSubscriptionsPurchaseType.swift | 37 ++ .../CustomerCenter/URLUtilities.swift | 32 ++ .../ViewModels/CustomerCenterViewModel.swift | 100 +++++ .../ViewModels/CustomerCenterViewState.swift | 37 ++ .../ManageSubscriptionsViewModel.swift | 190 +++++++++ .../Views/CustomerCenterView.swift | 100 +++++ .../Views/ManageSubscriptionsView.swift | 194 +++++++++ .../Views/NoSubscriptionsView.swift | 75 ++++ .../Views/RestorePurchasesAlert.swift | 123 ++++++ .../Views/WrongPlatformView.swift | 116 ++++++ RevenueCatUI/Data/Strings.swift | 5 + .../Resources/en.lproj/Localizable.strings | 11 + .../CustomerCenterViewModelTests.swift | 263 ++++++++++++ .../ManageSubscriptionsViewModelTests.swift | 390 ++++++++++++++++++ 22 files changed, 1975 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift create mode 100644 RevenueCatUI/CustomerCenter/URLUtilities.swift create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift create mode 100644 Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift create mode 100644 Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..bf01c5d8f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", + "lldb.launch.expressions": "native" +} \ No newline at end of file diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index b3a231eabf..ccab8908bd 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -184,6 +184,8 @@ 3543914226F911F300E669DF /* MockSK1Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */; }; 3543914426F911F300E669DF /* MockCustomerInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514826D44A2F00BD2BD7 /* MockCustomerInfoManager.swift */; }; 3543914526F926D900E669DF /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */; }; + 3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */; }; + 3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */; }; 354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */; }; 354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; @@ -1135,6 +1137,8 @@ 352B7D7827BD919B002A47DD /* DangerousSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DangerousSettings.swift; sourceTree = ""; }; 3530C18822653E8F00D6DF52 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; 35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsSynchronizer.swift; sourceTree = ""; }; + 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = ""; }; + 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsViewModelTests.swift; sourceTree = ""; }; 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionKey.swift; sourceTree = ""; }; 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; @@ -2565,6 +2569,15 @@ path = Purchasing; sourceTree = ""; }; + 3544DA6B2C2C848E00704E9D /* CustomerCenter */ = { + isa = PBXGroup; + children = ( + 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */, + 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */, + ); + path = CustomerCenter; + sourceTree = ""; + }; 354895D0267AE32D001DC5B1 /* SubscriberAttributes */ = { isa = PBXGroup; children = ( @@ -3470,6 +3483,7 @@ 887A62242C1D168B00E1A461 /* RevenueCatUITests */ = { isa = PBXGroup; children = ( + 3544DA6B2C2C848E00704E9D /* CustomerCenter */, 887A612D2C1D168B00E1A461 /* Data */, 887A61362C1D168B00E1A461 /* Helpers */, 887A61382C1D168B00E1A461 /* Purchasing */, @@ -5110,8 +5124,10 @@ 887A63352C1D177800E1A461 /* OSVersionEquivalent.swift in Sources */, 887A63362C1D177800E1A461 /* SnapshotTesting+Extensions.swift in Sources */, 887A63372C1D177800E1A461 /* TestCase.swift in Sources */, + 3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */, 887A63382C1D177800E1A461 /* PurchaseHandlerTests.swift in Sources */, 887A63392C1D177800E1A461 /* OtherPaywallViewTests.swift in Sources */, + 3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */, 887A633A2C1D177800E1A461 /* PaywallViewDynamicTypeTests.swift in Sources */, 887A633B2C1D177800E1A461 /* PaywallViewLocalizationTests.swift in Sources */, 887A633C2C1D177800E1A461 /* Template1ViewTests.swift in Sources */, diff --git a/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift index a5fc721b20..12685ef769 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift @@ -1,8 +1,63 @@ // -// File.swift +// 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 +// +// CustomerCenterConfigData.swift // // -// Created by Cesar de la Vega on 17/6/24. +// Created by Cesar de la Vega on 28/5/24. // import Foundation +import RevenueCat + +struct CustomerCenterConfigData { + + let id: String + let paths: [HelpPath] + let title: String + + enum HelpPathType: String { + case missingPurchase = "MISSING_PURCHASE" + case refundRequest = "REFUND_REQUEST" + case changePlans = "CHANGE_PLANS" + case cancel = "CANCEL" + case unknown + } + + enum HelpPathDetail { + + case promotionalOffer(PromotionalOffer) + case feedbackSurvey(FeedbackSurvey) + + } + + struct HelpPath { + + let id: String + let title: String + let type: HelpPathType + let detail: HelpPathDetail? + + } + + struct FeedbackSurvey { + + let title: String + let options: [FeedbackSurveyOption] + + } + + struct FeedbackSurveyOption { + + let id: String + let title: String + + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift new file mode 100644 index 0000000000..57b50e2d93 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -0,0 +1,79 @@ +// +// 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 +// +// CustomerCenterConfigTestData.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation +import RevenueCat + +enum CustomerCenterConfigTestData { + + @available(iOS 14.0, *) + static let customerCenterData = CustomerCenterConfigData( + id: "customer_center_id", + paths: [ + .init( + id: "1", + title: "Didn't receive purchase", + type: .missingPurchase, + detail: nil + ), + .init( + id: "2", + title: "Request a refund", + type: .refundRequest, + detail: nil + ), + .init( + id: "3", + title: "Change plans", + type: .changePlans, + detail: nil + ), + .init( + id: "4", + title: "Cancel subscription", + type: .cancel, + detail: .feedbackSurvey(.init( + title: "Why are you cancelling?", + options: [ + .init( + id: "1", + title: "Too expensive" + ), + .init( + id: "2", + title: "Don't use the app" + ), + .init( + id: "3", + title: "Bought by mistake" + ) + ] + )) + ) + ], + title: "How can we help?" + ) + + static let subscriptionInformation: SubscriptionInformation = .init( + title: "Basic", + durationTitle: "Monthly", + price: "$4.99 / month", + nextRenewalString: "June 1st, 2024", + willRenew: true, + productIdentifier: "product_id", + active: true + ) + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift new file mode 100644 index 0000000000..af5a2e1bfa --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -0,0 +1,41 @@ +// +// 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 +// +// CustomerCenterError.swift +// +// +// Created by Cesar de la Vega on 29/5/24. +// + +import Foundation + +/// Error produced when displaying the customer center. +enum CustomerCenterError: Error { + + /// Could not find information for an active subscription. + case couldNotFindSubscriptionInformation + +} + +extension CustomerCenterError: CustomNSError { + + var errorUserInfo: [String: Any] { + return [ + NSLocalizedDescriptionKey: self.description + ] + } + + private var description: String { + switch self { + case .couldNotFindSubscriptionInformation: + return "Could not find information for an active subscription." + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift new file mode 100644 index 0000000000..277120b163 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -0,0 +1,50 @@ +// +// 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 +// +// SubscriptionInformation.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +struct SubscriptionInformation { + + let title: String + let durationTitle: String + let price: String + let nextRenewalString: String? + let productIdentifier: String + + var renewalString: String { + return active ? (willRenew ? "Renews" : "Expires") : "Expired" + } + + private let willRenew: Bool + private let active: Bool + + init(title: String, + durationTitle: String, + price: String, + nextRenewalString: String?, + willRenew: Bool, + productIdentifier: String, + active: Bool + ) { + self.title = title + self.durationTitle = durationTitle + self.price = price + self.nextRenewalString = nextRenewalString + self.productIdentifier = productIdentifier + self.willRenew = willRenew + self.active = active + } + +} diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift new file mode 100644 index 0000000000..c4970d4ff7 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -0,0 +1,50 @@ +// +// 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 +// +// CustomButtonStyle.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation +import SwiftUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct ManageSubscriptionsButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding() + .frame(width: 300) + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(10) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .opacity(configuration.isPressed ? 0.8 : 1.0) + .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct CustomButtonStylePreview_Previews: PreviewProvider { + + static var previews: some View { + Button("Didn't receive purchase") {} + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + +} diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift new file mode 100644 index 0000000000..212f6ca568 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// ManageSubscriptionsPurchaseType.swift +// +// +// Created by Cesar de la Vega on 12/6/24. +// + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +protocol ManageSubscriptionsPurchaseType: Sendable { + + @Sendable + func customerInfo() async throws -> CustomerInfo + + @Sendable + func products(_ productIdentifiers: [String]) async -> [StoreProduct] + + @Sendable + func showManageSubscriptions() async throws + + @Sendable + func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus + +} diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift new file mode 100644 index 0000000000..2de1de68a6 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -0,0 +1,32 @@ +// +// 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 +// +// URLUtilities.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +enum URLUtilities { + + static func createMailURL() -> URL? { + let subject = "Support Request" + let body = "Please describe your issue or question." + let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + + // swiftlint:disable:next todo + // TODO: make configurable + let urlString = "mailto:support@revenuecat.com?subject=\(encodedSubject)&body=\(encodedBody)" + return URL(string: urlString) + } + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift new file mode 100644 index 0000000000..1be0d23a0d --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -0,0 +1,100 @@ +// +// 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 +// +// CustomerCenterViewModel.swift +// +// +// Created by Cesar de la Vega on 27/5/24. +// + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor class CustomerCenterViewModel: ObservableObject { + + typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo + + @Published + private(set) var hasSubscriptions: Bool = false + @Published + private(set) var subscriptionsAreFromApple: Bool = false + + // @PublicForExternalTesting + @Published + var state: CustomerCenterViewState { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } + + var isLoaded: Bool { + return state != .notLoaded + } + + private var customerInfoFetcher: CustomerInfoFetcher + + private var error: Error? + + convenience init() { + self.init(customerInfoFetcher: { + guard Purchases.isConfigured else { + throw PaywallError.purchasesNotConfigured + } + + return try await Purchases.shared.customerInfo() + }) + } + + // @PublicForExternalTesting + init(customerInfoFetcher: @escaping CustomerInfoFetcher) { + self.state = .notLoaded + self.customerInfoFetcher = customerInfoFetcher + } + + // @PublicForExternalTesting + init(hasSubscriptions: Bool = false, areSubscriptionsFromApple: Bool = false) { + self.hasSubscriptions = hasSubscriptions + self.subscriptionsAreFromApple = areSubscriptionsFromApple + self.customerInfoFetcher = { + guard Purchases.isConfigured else { + throw PaywallError.purchasesNotConfigured + } + + return try await Purchases.shared.customerInfo() + } + self.state = .success + } + + func loadHasSubscriptions() async { + do { + // swiftlint:disable:next todo + // TODO: support non-consumables + let customerInfo = try await self.customerInfoFetcher() + let hasSubscriptions = customerInfo.activeSubscriptions.count > 0 + + let subscriptionsAreFromApple = customerInfo.entitlements.active.contains(where: { entitlement in + entitlement.value.store == .appStore || entitlement.value.store == .macAppStore && + customerInfo.activeSubscriptions.contains(entitlement.value.productIdentifier) + }) + + self.hasSubscriptions = hasSubscriptions + self.subscriptionsAreFromApple = subscriptionsAreFromApple + self.state = .success + } catch { + self.state = .error(error) + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift new file mode 100644 index 0000000000..7a982112d0 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// CustomerCenterViewState.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Foundation + +enum CustomerCenterViewState: Equatable { + + case notLoaded + case success + case error(Error) + + static func == (lhs: CustomerCenterViewState, rhs: CustomerCenterViewState) -> Bool { + switch (lhs, rhs) { + case (.notLoaded, .notLoaded): + return true + case (.success, .success): + return true + case (let .error(lhsError), let .error(rhsError)): + return lhsError.localizedDescription == rhsError.localizedDescription + default: + return false + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift new file mode 100644 index 0000000000..57f14dd358 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -0,0 +1,190 @@ +// +// 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 +// +// ManageSubscriptionsViewModel.swift +// +// +// Created by Cesar de la Vega on 27/5/24. +// + +import Foundation +import RevenueCat + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class ManageSubscriptionsViewModel: ObservableObject { + + @Published + private(set) var subscriptionInformation: SubscriptionInformation? + @Published + private(set) var refundRequestStatusMessage: String? + @Published + private(set) var configuration: CustomerCenterConfigData? + @Published + var showRestoreAlert: Bool = false + @Published + var state: CustomerCenterViewState { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } + + var isLoaded: Bool { + return state != .notLoaded + } + + private var purchasesProvider: ManageSubscriptionsPurchaseType + + private var error: Error? + + convenience init() { + self.init(purchasesProvider: ManageSubscriptionPurchases()) + } + + // @PublicForExternalTesting + init(purchasesProvider: ManageSubscriptionsPurchaseType) { + self.state = .notLoaded + self.purchasesProvider = purchasesProvider + } + + // @PublicForExternalTesting + init(configuration: CustomerCenterConfigData, + subscriptionInformation: SubscriptionInformation) { + self.configuration = configuration + self.subscriptionInformation = subscriptionInformation + self.purchasesProvider = ManageSubscriptionPurchases() + state = .success + } + + func loadScreen() async { + do { + try await loadSubscriptionInformation() + loadCustomerCenterConfig() + self.state = .success + } catch { + self.state = .error(error) + } + } + + private func loadSubscriptionInformation() async throws { + let customerInfo = try await purchasesProvider.customerInfo() + guard let currentEntitlementDict = customerInfo.entitlements.active.first, + let subscribedProductID = customerInfo.activeSubscriptions.first, + let subscribedProduct = await purchasesProvider.products([subscribedProductID]).first else { + Logger.warning(Strings.could_not_find_subscription_information) + throw CustomerCenterError.couldNotFindSubscriptionInformation + } + let currentEntitlement = currentEntitlementDict.value + + // swiftlint:disable:next todo + // TODO: support non-consumables + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + self.subscriptionInformation = SubscriptionInformation( + title: subscribedProduct.localizedTitle, + durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", + price: subscribedProduct.localizedPriceString, + nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, + willRenew: currentEntitlement.willRenew, + productIdentifier: subscribedProductID, + active: currentEntitlement.isActive + ) + } + + private func loadCustomerCenterConfig() { + self.configuration = CustomerCenterConfigTestData.customerCenterData + } + + #if os(iOS) || targetEnvironment(macCatalyst) + func handleAction(for path: CustomerCenterConfigData.HelpPath) async { + switch path.type { + case .missingPurchase: + self.showRestoreAlert = true + case .refundRequest: + do { + guard let subscriptionInformation = self.subscriptionInformation else { return } + let productId = subscriptionInformation.productIdentifier + let status = try await purchasesProvider.beginRefundRequest(forProduct: productId) + switch status { + case .error: + self.refundRequestStatusMessage = String(localized: "Error when requesting refund, try again") + case .success: + self.refundRequestStatusMessage = String(localized: "Refund granted successfully!") + case .userCancelled: + self.refundRequestStatusMessage = String(localized: "Refund canceled") + } + } catch { + self.refundRequestStatusMessage = + String(localized: "An error occurred while processing the refund request.") + } + case .changePlans, .cancel: + do { + try await purchasesProvider.showManageSubscriptions() + } catch { + self.state = .error(error) + } + default: + break + } + } + #endif + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType { + + func beginRefundRequest(forProduct productID: String) async throws -> RevenueCat.RefundRequestStatus { + try await Purchases.shared.beginRefundRequest(forProduct: productID) + } + + func showManageSubscriptions() async throws { + try await Purchases.shared.showManageSubscriptions() + } + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + try await Purchases.shared.customerInfo() + } + + func products(_ productIdentifiers: [String]) async -> [StoreProduct] { + await Purchases.shared.products(productIdentifiers) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private extension SubscriptionPeriod { + + var durationTitle: String { + switch self.unit { + case .day: return "day" + case .week: return "week" + case .month: return "month" + case .year: return "year" + default: return "Unknown" + } + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift new file mode 100644 index 0000000000..d7c843bfbf --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -0,0 +1,100 @@ +// +// 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 +// +// CustomerCenterView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import RevenueCat +import SwiftUI + +#if os(iOS) + +/// A SwiftUI view for displaying a customer support common tasks +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +public struct CustomerCenterView: View { + + @StateObject private var viewModel = CustomerCenterViewModel() + + /// Create a view to handle common customer support tasks + public init() {} + + fileprivate init(viewModel: CustomerCenterViewModel) { + self._viewModel = .init(wrappedValue: viewModel) + } + + // swiftlint:disable:next missing_docs + public var body: some View { + Group { + if !viewModel.isLoaded { + ProgressView() + } else { + destinationView() + } + } + .task { + await checkAndLoadSubscriptions() + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +private extension CustomerCenterView { + + func checkAndLoadSubscriptions() async { + if !viewModel.isLoaded { + await viewModel.loadHasSubscriptions() + } + } + + @ViewBuilder + func destinationView() -> some View { + if viewModel.hasSubscriptions { + if viewModel.subscriptionsAreFromApple { + ManageSubscriptionsView() + } else { + WrongPlatformView() + } + } else { + NoSubscriptionsView() + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct CustomerCenterView_Previews: PreviewProvider { + + static var previews: some View { + let viewModel = CustomerCenterViewModel(hasSubscriptions: false, areSubscriptionsFromApple: false) + CustomerCenterView(viewModel: viewModel) + } + +} + +#endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift new file mode 100644 index 0000000000..08869af465 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -0,0 +1,194 @@ +// +// 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 +// +// ManageSubscriptionsView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct ManageSubscriptionsView: View { + + @Environment(\.openURL) + var openURL + + @StateObject + private var viewModel = ManageSubscriptionsViewModel() + + init() { } + + fileprivate init(viewModel: ManageSubscriptionsViewModel) { + self._viewModel = .init(wrappedValue: viewModel) + } + + var body: some View { + VStack { + if viewModel.isLoaded { + HeaderView(viewModel: viewModel) + + if let subscriptionInformation = self.viewModel.subscriptionInformation { + SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, + refundRequestStatusMessage: viewModel.refundRequestStatusMessage) + } + + Spacer() + + ManageSubscriptionsButtonsView(viewModel: viewModel) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + .task { + await loadInformationIfNeeded() + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +private extension ManageSubscriptionsView { + + func loadInformationIfNeeded() async { + if !viewModel.isLoaded { + await viewModel.loadScreen() + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct HeaderView: View { + + @ObservedObject + private(set) var viewModel: ManageSubscriptionsViewModel + + var body: some View { + if let configuration = viewModel.configuration { + Text(configuration.title) + .font(.title) + .padding() + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct SubscriptionDetailsView: View { + + let subscriptionInformation: SubscriptionInformation + let refundRequestStatusMessage: String? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("\(subscriptionInformation.title) - \(subscriptionInformation.durationTitle)") + .font(.subheadline) + .padding([.horizontal, .top]) + + Text("\(subscriptionInformation.price)") + .font(.caption) + .foregroundColor(Color.gray) + .padding(.horizontal) + + if let nextRenewal = subscriptionInformation.nextRenewalString { + Text("\(subscriptionInformation.renewalString): \(String(describing: nextRenewal))") + .font(.caption) + .foregroundColor(Color.gray) + .padding([.horizontal, .bottom]) + } + + if let refundRequestStatusMessage = refundRequestStatusMessage { + Text("Refund request status: \(refundRequestStatusMessage)") + .font(.caption) + .bold() + .foregroundColor(Color.gray) + .padding([.horizontal, .bottom]) + } + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct ManageSubscriptionsButtonsView: View { + + @ObservedObject + private(set) var viewModel: ManageSubscriptionsViewModel + + var body: some View { + VStack(spacing: 16) { + if let configuration = viewModel.configuration { + let filteredPaths = configuration.paths.filter { path in + #if targetEnvironment(macCatalyst) + return path.type == .refundRequest + #else + return true + #endif + } + ForEach(filteredPaths, id: \.id) { path in + AsyncButton(action: { + await self.viewModel.handleAction(for: path) + }, label: { + Text(path.title) + }) + .restorePurchasesAlert(isPresented: $viewModel.showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + } + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct ManageSubscriptionsView_Previews: PreviewProvider { + + static var previews: some View { + let viewModel = ManageSubscriptionsViewModel( + configuration: CustomerCenterConfigTestData.customerCenterData, + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformation) + ManageSubscriptionsView(viewModel: viewModel) + } + +} + +#endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift new file mode 100644 index 0000000000..58cecf658f --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -0,0 +1,75 @@ +// +// 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 +// +// NoSubscriptionsView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct NoSubscriptionsView: View { + + @Environment(\.dismiss) var dismiss + @State private var showRestoreAlert: Bool = false + + var body: some View { + VStack { + Text("No Subscriptions found") + .font(.title) + .padding() + Text("We can try checking your Apple account for any previous purchases") + .font(.body) + .padding() + + Spacer() + + Button("Restore purchases") { + showRestoreAlert = true + } + .restorePurchasesAlert(isPresented: $showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) + + Button("Cancel") { + dismiss() + } + + } + + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct NoSubscriptionsView_Previews: PreviewProvider { + + static var previews: some View { + NoSubscriptionsView() + } + +} + +#endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift new file mode 100644 index 0000000000..0eff6cc2ce --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -0,0 +1,123 @@ +// +// 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 +// +// RestorePurchasesAlert.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import Foundation +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct RestorePurchasesAlert: ViewModifier { + + @Binding + var isPresented: Bool + @Environment(\.openURL) + var openURL + + @State + private var alertType: AlertType = .restorePurchases + @Environment(\.dismiss) + private var dismiss + + enum AlertType: Identifiable { + case purchasesRecovered, purchasesNotFound, restorePurchases + var id: Self { self } + } + + func body(content: Content) -> some View { + content + .alert(isPresented: $isPresented) { + switch self.alertType { + case .restorePurchases: + return Alert( + title: Text("Restore purchases"), + message: Text( + """ + Let’s take a look! We’re going to check your Apple account for missing purchases. + """), + primaryButton: .default(Text("Check past purchases"), action: { + Task { + guard let customerInfo = try? await Purchases.shared.restorePurchases() else { + // todo: handle errors + self.setAlertType(.purchasesNotFound) + return + } + let hasEntitlements = customerInfo.entitlements.active.count > 0 + if hasEntitlements { + self.setAlertType(.purchasesRecovered) + } else { + self.setAlertType(.purchasesNotFound) + } + } + }), + secondaryButton: .cancel(Text("Cancel")) + ) + + case .purchasesRecovered: + return Alert(title: Text("Purchases recovered!"), + message: Text("We applied the previously purchased items to your account. " + + "Sorry for the inconvenience."), + dismissButton: .default(Text("Dismiss")) { + dismiss() + }) + + case .purchasesNotFound: + return Alert(title: Text(""), + message: Text("We couldn’t find any additional purchases under this account. \n\n" + + "Contact support for assistance if you think this is an error."), + dismissButton: .default(Text("Dismiss")) { + dismiss() + }) + } + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +private extension RestorePurchasesAlert { + + func setAlertType(_ newType: AlertType) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.alertType = newType + self.isPresented = true + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension View { + + func restorePurchasesAlert(isPresented: Binding) -> some View { + self.modifier(RestorePurchasesAlert(isPresented: isPresented)) + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift new file mode 100644 index 0000000000..1f1fe1ef77 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -0,0 +1,116 @@ +// +// 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 +// +// WrongPlatformView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import Foundation +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct WrongPlatformView: View { + + @State + private var store: Store? + + @Environment(\.openURL) + private var openURL + + init() { + } + + fileprivate init(store: Store) { + self._store = State(initialValue: store) + } + + var body: some View { + VStack { + + switch store { + case .appStore, .macAppStore, .playStore, .amazon: + let platformName = humanReadablePlatformName(store: store!) + + Text("Your subscription is a \(platformName) subscription.") + .font(.title) + .padding() + Text("Go the app settings on \(platformName) to manage your subscription and billing.") + .padding() + default: + Text("Please contact support to manage your subscription") + .font(.title) + .padding() + } + + } + .task { + if store == nil { + if let customerInfo = try? await Purchases.shared.customerInfo(), + let firstEntitlement = customerInfo.entitlements.active.first { + self.store = firstEntitlement.value.store + } + } + } + } + + private func humanReadablePlatformName(store: Store) -> String { + switch store { + case .appStore, .macAppStore: + return "Apple App Store" + case .playStore: + return "Google Play Store" + case .stripe, + .rcBilling, + .external: + return "Web" + case .promotional: + return "Free" + case .amazon: + return "Amazon Appstore" + case .unknownStore: + return "Unknown" + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct WrongPlatformView_Previews: PreviewProvider { + + static var previews: some View { + Group { + WrongPlatformView(store: .appStore) + .previewDisplayName("App Store") + + WrongPlatformView(store: .rcBilling) + .previewDisplayName("RCBilling") + } + + } + +} + +#endif + +#endif diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index b8e3e48f12..5f9e81ddad 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -43,6 +43,8 @@ enum Strings { case executing_external_purchase_logic case executing_restore_logic case executing_external_restore_logic + + case could_not_find_subscription_information } @@ -117,6 +119,9 @@ extension Strings: CustomStringConvertible { return "Will execute custom StoreKit restore purchases logic provided by your app. " + "No StoreKit restore purchases logic will be performed by RevenueCat. " + "You must have initialized your `PaywallView` appropriately." + + case .could_not_find_subscription_information: + return "Could not find any active subscription's information" } } diff --git a/RevenueCatUI/Resources/en.lproj/Localizable.strings b/RevenueCatUI/Resources/en.lproj/Localizable.strings index eb59e1d760..9037347987 100644 --- a/RevenueCatUI/Resources/en.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en.lproj/Localizable.strings @@ -17,3 +17,14 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"Error when requesting refund, try again" = "Error when requesting refund, try again"; +"Refund granted successfully!" = "Refund granted successfully!"; +"Refund canceled" = "Refund canceled"; +"Refund request status: %@" = "Refund request status: %@"; +"Let’s take a look! We’re going to check your Apple account for missing purchases." = "Let’s take a look! We’re going to check your Apple account for missing purchases."; +"Check past purchases" = "Check past purchases"; +"Cancel" = "Cancel"; +"Purchases recovered!" = "Purchases recovered!"; +"We applied the previously purchased items to your account. Sorry for the inconvenience." = "We applied the previously purchased items to your account. Sorry for the inconvenience."; +"Dismiss" = "Dismiss"; +"We couldn’t find any additional purchases under this account. \n\nContact support for assistance if you think this is an error." = "We couldn’t find any additional purchases under this account. \n\nContact support for assistance if you think this is an error."; diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift new file mode 100644 index 0000000000..1adb2d6b4d --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -0,0 +1,263 @@ +// +// 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 +// +// CustomerCenterViewModelTests.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import XCTest + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class CustomerCenterViewModelTests: TestCase { + + private let error = TestError(message: "An error occurred") + + private struct TestError: Error, Equatable { + let message: String + var localizedDescription: String { + return message + } + } + + func testInitialState() { + let viewModel = CustomerCenterViewModel() + + expect(viewModel.state) == .notLoaded + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.isLoaded) == false + } + + func testStateChangeToError() { + let viewModel = CustomerCenterViewModel() + + viewModel.state = .error(error) + + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + + func testIsLoaded() { + let viewModel = CustomerCenterViewModel() + + expect(viewModel.isLoaded) == false + + viewModel.state = .success + + expect(viewModel.isLoaded) == true + } + + func testLoadHasSubscriptionsApple() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == true + expect(viewModel.subscriptionsAreFromApple) == true + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsGoogle() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == true + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsNonActive() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsFailure() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + throw TestError(message: "An error occurred") + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension CustomerCenterViewModelTests { + + static let customerInfoWithAppleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithGoogleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithoutSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2000-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "1999-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "1999-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2000-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "1999-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + +} + +#endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift new file mode 100644 index 0000000000..8519697913 --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -0,0 +1,390 @@ +// +// 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 +// +// ManageSubscriptionsViewModelTests.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import StoreKit +import XCTest + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class ManageSubscriptionsViewModelTests: TestCase { + + private let error = TestError(message: "An error occurred") + + private struct TestError: Error, Equatable { + let message: String + var localizedDescription: String { + return message + } + } + + func testInitialState() { + let viewModel = ManageSubscriptionsViewModel() + + expect(viewModel.state) == .notLoaded + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.refundRequestStatusMessage).to(beNil()) + expect(viewModel.configuration).to(beNil()) + expect(viewModel.showRestoreAlert) == false + expect(viewModel.isLoaded) == false + } + + func testStateChangeToError() { + let viewModel = ManageSubscriptionsViewModel() + + viewModel.state = .error(error) + + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + + func testIsLoaded() { + let viewModel = ManageSubscriptionsViewModel() + + expect(viewModel.isLoaded) == false + + viewModel.state = .success + + expect(viewModel.isLoaded) == true + } + + func testLoadScreenSuccess() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases()) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).toNot(beNil()) + expect(viewModel.configuration).toNot(beNil()) + expect(viewModel.state) == .success + + expect(viewModel.subscriptionInformation?.title) == "title" + expect(viewModel.subscriptionInformation?.durationTitle) == "month" + expect(viewModel.subscriptionInformation?.price) == "$2.99" + expect(viewModel.subscriptionInformation?.nextRenewalString) == "Apr 12, 2062" + expect(viewModel.subscriptionInformation?.productIdentifier) == "com.revenuecat.product" + } + + func testLoadScreenNoActiveSubscription() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + )) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.state) == .error(CustomerCenterError.couldNotFindSubscriptionInformation) + } + + func testLoadScreenFailure() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( + customerInfoError: error + )) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.state) == .error(error) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { + + let customerInfo: CustomerInfo? + let customerInfoError: Error? + let productsShouldFail: Bool + let showManageSubscriptionsError: Error? + let beginRefundShouldFail: Bool + + init( + customerInfo: CustomerInfo? = nil, + customerInfoError: Error? = nil, + productsShouldFail: Bool = false, + showManageSubscriptionsError: Error? = nil, + beginRefundShouldFail: Bool = false + ) { + self.customerInfo = customerInfo + self.customerInfoError = customerInfoError + self.productsShouldFail = productsShouldFail + self.showManageSubscriptionsError = showManageSubscriptionsError + self.beginRefundShouldFail = beginRefundShouldFail + } + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + if let customerInfoError { + throw customerInfoError + } + if let customerInfo { + return customerInfo + } + return CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + } + + func products(_ productIdentifiers: [String]) async -> [RevenueCat.StoreProduct] { + if productsShouldFail { + return [] + } + let product = await CustomerCenterViewModelTests.createMockProduct() + return [product] + } + + func showManageSubscriptions() async throws { + if let showManageSubscriptionsError { + throw showManageSubscriptionsError + } + } + + func beginRefundRequest(forProduct productID: String) async throws -> RevenueCat.RefundRequestStatus { + if beginRefundShouldFail { + return .error + } + return .success + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension CustomerCenterViewModelTests { + + static func createMockProduct() -> StoreProduct { + // Using SK1 products because they can be mocked, but CustomerCenterViewModel + // works with generic `StoreProduct`s regardless of what they contain + return StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "identifier", + mockLocalizedTitle: "title")) + } + + static let customerInfoWithAppleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithGoogleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithoutSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2000-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "1999-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "1999-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2000-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "1999-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private class MockSK1Product: SK1Product { + var mockProductIdentifier: String + var mockLocalizedTitle: String + + init(mockProductIdentifier: String, mockLocalizedTitle: String) { + self.mockProductIdentifier = mockProductIdentifier + self.mockLocalizedTitle = mockLocalizedTitle + + super.init() + } + + override var productIdentifier: String { + return self.mockProductIdentifier + } + + var mockSubscriptionGroupIdentifier: String? + override var subscriptionGroupIdentifier: String? { + return self.mockSubscriptionGroupIdentifier + } + + var mockPriceLocale: Locale? + override var priceLocale: Locale { + return mockPriceLocale ?? Locale(identifier: "en_US") + } + + var mockPrice: Decimal? + override var price: NSDecimalNumber { + return (mockPrice ?? 2.99) as NSDecimalNumber + } + + override var localizedTitle: String { + return self.mockLocalizedTitle + } + + override var introductoryPrice: SKProductDiscount? { + return mockDiscount + } + + private var _mockDiscount: Any? + + var mockDiscount: SKProductDiscount? { + // swiftlint:disable:next force_cast + get { return self._mockDiscount as! SKProductDiscount? } + set { self._mockDiscount = newValue } + } + + override var discounts: [SKProductDiscount] { + return self.mockDiscount.map { [$0] } ?? [] + } + + private lazy var _mockSubscriptionPeriod: Any? = { + return SKProductSubscriptionPeriod(numberOfUnits: 1, unit: SKProduct.PeriodUnit.month) + }() + + var mockSubscriptionPeriod: SKProductSubscriptionPeriod? { + // swiftlint:disable:next force_cast + get { self._mockSubscriptionPeriod as! SKProductSubscriptionPeriod? } + set { self._mockSubscriptionPeriod = newValue } + } + + override var subscriptionPeriod: SKProductSubscriptionPeriod? { + return mockSubscriptionPeriod + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate extension SKProductSubscriptionPeriod { + convenience init(numberOfUnits: Int, + unit: SK1Product.PeriodUnit) { + self.init() + self.setValue(numberOfUnits, forKey: "numberOfUnits") + self.setValue(unit.rawValue, forKey: "unit") + } +} + +#endif From 9bc2c0826674e0a7ff86f6343b27ca23170a3322 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 9 Jul 2024 21:48:30 +0200 Subject: [PATCH 03/90] [Customer Center] Add API call to get Customer Center config (#3933) Adds `Purchases.shared.loadCustomerCenter()` that calls a new backend endpoint that returns the customer center configuration This API call doesn't exist yet and it will change. This PR is the ground work so that we don't have to wait for the backend to add this API and we can already pretend the API is there. --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com> --- .gitignore | 1 + RevenueCat.xcodeproj/project.pbxproj | 146 ++++++- .../Data/CustomerCenterConfigData.swift | 63 --- .../Data/CustomerCenterConfigTestData.swift | 104 +++-- .../ViewModels/CustomerCenterViewModel.swift | 12 +- .../ManageSubscriptionsViewModel.swift | 34 +- .../Views/CustomerCenterView.swift | 18 +- .../Views/ManageSubscriptionsView.swift | 59 ++- .../Views/NoSubscriptionsView.swift | 17 +- RevenueCatUI/Data/Strings.swift | 4 +- .../CustomerCenterConfigData.swift | 313 +++++++++++++++ Sources/Networking/Backend.swift | 9 +- .../CustomerCenterConfigCallback.swift | 23 ++ .../Networking/CustomerCenterConfigAPI.swift | 50 +++ .../HTTPClient/HTTPRequestPath.swift | 19 +- .../GetCustomerCenterConfigOperation.swift | 87 ++++ .../CustomerCenterConfigResponse.swift | 137 +++++++ Sources/Purchasing/Purchases/Purchases.swift | 12 + .../StorefrontProvider.swift | 2 +- .../SwiftAPITester.xcodeproj/project.pbxproj | 4 + .../CustomerCenterConfigDataAPI.swift | 85 ++++ .../CustomerCenterViewModelTests.swift | 1 + .../ManageSubscriptionsViewModelTests.swift | 34 +- .../CustomerCenterConfigDataTests.swift | 124 ++++++ Tests/UnitTests/Mocks/MockBackend.swift | 4 +- .../BackendGetCustomerCenterConfigTests.swift | 377 ++++++++++++++++++ .../Networking/Backend/BaseBackendTest.swift | 5 +- .../iOS14-testGetCustomerCenterConfig.1.json | 25 ++ ...omerCenterConfigCachesForSameUserID.1.json | 25 ++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 25 ++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 25 ++ ...GetCustomerCenterConfigFailSendsNil.1.json | 25 ++ ...rCenterConfigNetworkErrorSendsError.1.json | 25 ++ ...etCustomerCenterConfigPassesLocales.1.json | 25 ++ ...rConfigDoesntCacheForMultipleUserID.1.json | 25 ++ ...rConfigDoesntCacheForMultipleUserID.2.json | 25 ++ ...testRepeatedRequestsLogDebugMessage.1.json | 25 ++ .../iOS17-testGetCustomerCenterConfig.1.json | 25 ++ ...omerCenterConfigCachesForSameUserID.1.json | 25 ++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 25 ++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 25 ++ ...GetCustomerCenterConfigFailSendsNil.1.json | 25 ++ ...rCenterConfigNetworkErrorSendsError.1.json | 25 ++ ...etCustomerCenterConfigPassesLocales.1.json | 25 ++ ...rConfigDoesntCacheForMultipleUserID.1.json | 25 ++ ...rConfigDoesntCacheForMultipleUserID.2.json | 25 ++ ...testRepeatedRequestsLogDebugMessage.1.json | 25 ++ .../macOS-testGetCustomerCenterConfig.1.json | 24 ++ ...omerCenterConfigCachesForSameUserID.1.json | 24 ++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 24 ++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 24 ++ ...GetCustomerCenterConfigFailSendsNil.1.json | 24 ++ ...rCenterConfigNetworkErrorSendsError.1.json | 24 ++ ...etCustomerCenterConfigPassesLocales.1.json | 24 ++ ...rConfigDoesntCacheForMultipleUserID.1.json | 24 ++ ...rConfigDoesntCacheForMultipleUserID.2.json | 24 ++ ...testRepeatedRequestsLogDebugMessage.1.json | 24 ++ .../iOS17-testEncodesCustomerUserID.1.json | 10 - 58 files changed, 2296 insertions(+), 198 deletions(-) delete mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift create mode 100644 Sources/CustomerCenter/CustomerCenterConfigData.swift create mode 100644 Sources/Networking/Caching/CustomerCenterConfigCallback.swift create mode 100644 Sources/Networking/CustomerCenterConfigAPI.swift create mode 100644 Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift create mode 100644 Sources/Networking/Responses/CustomerCenterConfigResponse.swift create mode 100644 Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift create mode 100644 Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift create mode 100644 Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testRepeatedRequestsLogDebugMessage.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json delete mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerInfoTests/iOS17-testEncodesCustomerUserID.1.json diff --git a/.gitignore b/.gitignore index e62baebfe5..1fda2d3835 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ Tests/InstallationTests/XcodeDirectInstallation/XcodeDirectInstallation.xcodepro # fastlane fastlane/.env +.vscode/settings.json diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index ccab8908bd..665727fe43 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -176,6 +176,21 @@ 35272E2226D0048D00F22C3B /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353CBE9CF2572A72A347F /* HTTPClientTests.swift */; }; 352B7D7927BD919B002A47DD /* DangerousSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352B7D7827BD919B002A47DD /* DangerousSettings.swift */; }; 35316DAA2BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */; }; + 353756522C382BC700A1B8D6 /* PreferredLocalesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756512C382BC700A1B8D6 /* PreferredLocalesProvider.swift */; }; + 353756652C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */; }; + 353756662C382C2800A1B8D6 /* CustomerCenterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */; }; + 353756672C382C2800A1B8D6 /* SubscriptionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */; }; + 353756682C382C2800A1B8D6 /* CustomerCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756572C382C2800A1B8D6 /* CustomerCenterViewModel.swift */; }; + 353756692C382C2800A1B8D6 /* CustomerCenterViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756582C382C2800A1B8D6 /* CustomerCenterViewState.swift */; }; + 3537566A2C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756592C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift */; }; + 3537566B2C382C2800A1B8D6 /* CustomerCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */; }; + 3537566C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */; }; + 3537566D2C382C2800A1B8D6 /* NoSubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */; }; + 3537566E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */; }; + 3537566F2C382C2800A1B8D6 /* WrongPlatformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */; }; + 353756702C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */; }; + 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */; }; + 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756632C382C2800A1B8D6 /* URLUtilities.swift */; }; 3543913626F90D6A00E669DF /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */; }; 3543913826F90FE100E669DF /* MockIntroEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515B26D44B7900BD2BD7 /* MockIntroEligibilityCalculator.swift */; }; 3543913926F90FFB00E669DF /* MockBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514026D4498F00BD2BD7 /* MockBackend.swift */; }; @@ -189,7 +204,12 @@ 354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */; }; 354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; - 358C756C2C332BE800ECCA12 /* PreferredLocalesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358C756B2C332BE800ECCA12 /* PreferredLocalesProvider.swift */; }; + 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; + 3592E8862C2ED51700D7F91D /* CustomerCenterConfigCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */; }; + 3592E88A2C2ED54A00D7F91D /* CustomerCenterConfigData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */; }; + 3592E88C2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E88B2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift */; }; + 3592E88E2C2ED5B200D7F91D /* CustomerCenterConfigResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E88D2C2ED5B200D7F91D /* CustomerCenterConfigResponse.swift */; }; + 3592E8902C2ED5C100D7F91D /* CustomerCenterConfigAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E88F2C2ED5C100D7F91D /* CustomerCenterConfigAPI.swift */; }; 359E8E3F26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359E8E3E26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift */; }; 35AAEB452BBB14D000A12548 /* DiagnosticsFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AAEB442BBB14D000A12548 /* DiagnosticsFileHandler.swift */; }; 35AAEB492BBB17B500A12548 /* DiagnosticsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AAEB482BBB17B500A12548 /* DiagnosticsEvent.swift */; }; @@ -212,6 +232,7 @@ 35D83312262FBD4200E60AC5 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; }; 35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */; }; 35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; }; + 35F38B482C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */; }; 35F82BAB26A84E130051DF03 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */; }; 35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */; }; 35F82BB426A9A74D0051DF03 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */; }; @@ -1137,13 +1158,33 @@ 352B7D7827BD919B002A47DD /* DangerousSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DangerousSettings.swift; sourceTree = ""; }; 3530C18822653E8F00D6DF52 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; 35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsSynchronizer.swift; sourceTree = ""; }; + 353756512C382BC700A1B8D6 /* PreferredLocalesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferredLocalesProvider.swift; sourceTree = ""; }; + 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigTestData.swift; sourceTree = ""; }; + 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterError.swift; sourceTree = ""; }; + 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionInformation.swift; sourceTree = ""; }; + 353756572C382C2800A1B8D6 /* CustomerCenterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModel.swift; sourceTree = ""; }; + 353756582C382C2800A1B8D6 /* CustomerCenterViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewState.swift; sourceTree = ""; }; + 353756592C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsViewModel.swift; sourceTree = ""; }; + 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterView.swift; sourceTree = ""; }; + 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsView.swift; sourceTree = ""; }; + 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoSubscriptionsView.swift; sourceTree = ""; }; + 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePurchasesAlert.swift; sourceTree = ""; }; + 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WrongPlatformView.swift; sourceTree = ""; }; + 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsButtonStyle.swift; sourceTree = ""; }; + 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsPurchaseType.swift; sourceTree = ""; }; + 353756632C382C2800A1B8D6 /* URLUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLUtilities.swift; sourceTree = ""; }; 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = ""; }; 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsViewModelTests.swift; sourceTree = ""; }; 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionKey.swift; sourceTree = ""; }; 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; + 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; 357C9BC022725CFA006BC624 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/iAd.framework; sourceTree = DEVELOPER_DIR; }; - 358C756B2C332BE800ECCA12 /* PreferredLocalesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferredLocalesProvider.swift; sourceTree = ""; }; + 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigCallback.swift; sourceTree = ""; }; + 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigData.swift; sourceTree = ""; }; + 3592E88B2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GetCustomerCenterConfigOperation.swift; path = Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift; sourceTree = SOURCE_ROOT; }; + 3592E88D2C2ED5B200D7F91D /* CustomerCenterConfigResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigResponse.swift; sourceTree = ""; }; + 3592E88F2C2ED5C100D7F91D /* CustomerCenterConfigAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigAPI.swift; sourceTree = ""; }; 3597020F24BF6A710010506E /* TransactionsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsFactory.swift; sourceTree = ""; }; 3597021124BF6AAC0010506E /* TransactionsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsFactoryTests.swift; sourceTree = ""; }; 359E8E3E26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityChecker.swift; sourceTree = ""; }; @@ -1166,6 +1207,7 @@ 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK1Tests.swift; sourceTree = ""; }; 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsHelper.swift; sourceTree = ""; }; 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManageSubscriptionsHelper.swift; sourceTree = ""; }; + 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendGetCustomerCenterConfigTests.swift; sourceTree = ""; }; 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = ""; }; 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionDataMigratorTests.swift; sourceTree = ""; }; 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; @@ -2130,6 +2172,7 @@ 2DC5621724EC63420031F69B /* Sources */ = { isa = PBXGroup; children = ( + 3592E8892C2ED54A00D7F91D /* CustomerCenter */, F5BE44412698580200254A30 /* Attribution */, B3B5FBBD269E080A00104A0C /* Caching */, F5714EA626D7A82E00635477 /* CodableExtensions */, @@ -2158,6 +2201,7 @@ 2DC5622224EC63430031F69B /* UnitTests */ = { isa = PBXGroup; children = ( + 357348FE2C3BEAF8000EEB86 /* CustomerCenter */, F5BE444626985E6E00254A30 /* Attribution */, 37E35AE0CDC4C2AA8260FB58 /* Caching */, 4F2F2F122A3CEA9E00652B24 /* Diagnostics */, @@ -2205,7 +2249,7 @@ 2DDA3E4524DB0B4500EDFE5B /* Misc */ = { isa = PBXGroup; children = ( - 358C75682C332BAE00ECCA12 /* Locale */, + 35F38B492C32BC2800CD29FD /* Locale */, 57F3C0CA29B7A08F0004FD7E /* Codable */, 57F3C0CB29B7A0B10004FD7E /* Concurrency */, 352B7D7827BD919B002A47DD /* DangerousSettings.swift */, @@ -2500,6 +2544,51 @@ name = Frameworks; sourceTree = ""; }; + 353756562C382C2800A1B8D6 /* Data */ = { + isa = PBXGroup; + children = ( + 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, + 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, + 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */, + ); + path = Data; + sourceTree = ""; + }; + 3537565A2C382C2800A1B8D6 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 353756572C382C2800A1B8D6 /* CustomerCenterViewModel.swift */, + 353756582C382C2800A1B8D6 /* CustomerCenterViewState.swift */, + 353756592C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 353756602C382C2800A1B8D6 /* Views */ = { + isa = PBXGroup; + children = ( + 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */, + 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */, + 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */, + 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, + 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 353756642C382C2800A1B8D6 /* CustomerCenter */ = { + isa = PBXGroup; + children = ( + 353756562C382C2800A1B8D6 /* Data */, + 3537565A2C382C2800A1B8D6 /* ViewModels */, + 353756602C382C2800A1B8D6 /* Views */, + 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */, + 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */, + 353756632C382C2800A1B8D6 /* URLUtilities.swift */, + ); + path = CustomerCenter; + sourceTree = ""; + }; 354235D524C11138008C84EE /* Purchasing */ = { isa = PBXGroup; children = ( @@ -2590,12 +2679,20 @@ path = SubscriberAttributes; sourceTree = ""; }; - 358C75682C332BAE00ECCA12 /* Locale */ = { + 357348FE2C3BEAF8000EEB86 /* CustomerCenter */ = { isa = PBXGroup; children = ( - 358C756B2C332BE800ECCA12 /* PreferredLocalesProvider.swift */, + 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */, ); - path = Locale; + path = CustomerCenter; + sourceTree = ""; + }; + 3592E8892C2ED54A00D7F91D /* CustomerCenter */ = { + isa = PBXGroup; + children = ( + 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */, + ); + path = CustomerCenter; sourceTree = ""; }; 35D159C72BC438C6004D8061 /* Networking */ = { @@ -2619,6 +2716,7 @@ B3C4AAD426B8911300E1B3C8 /* Backend.swift */, B37815482857F1E7000A7B93 /* BackendConfiguration.swift */, B34605D0279A6E600031CA74 /* CustomerAPI.swift */, + 3592E88F2C2ED5C100D7F91D /* CustomerCenterConfigAPI.swift */, B3781567285A79FC000A7B93 /* IdentityAPI.swift */, B378156B285A9729000A7B93 /* OfferingsAPI.swift */, 57488BC529CB7BDC0000EE7E /* OfflineEntitlementsAPI.swift */, @@ -2662,6 +2760,14 @@ path = Support; sourceTree = ""; }; + 35F38B492C32BC2800CD29FD /* Locale */ = { + isa = PBXGroup; + children = ( + 353756512C382BC700A1B8D6 /* PreferredLocalesProvider.swift */, + ); + path = Locale; + sourceTree = ""; + }; 35F82BBB26A9BFA60051DF03 /* FoundationExtensions */ = { isa = PBXGroup; children = ( @@ -3064,6 +3170,7 @@ 57DB164F298C4327008F6707 /* BackendSignatureVerificationTests.swift */, 5796A38027D6B78500653165 /* BaseBackendTest.swift */, 35109DAA2BC6E436001030C8 /* BackendPostDiagnosticsTests.swift */, + 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */, ); path = Backend; sourceTree = ""; @@ -3089,6 +3196,7 @@ 57D5412C27F63108004CC35C /* Responses */ = { isa = PBXGroup; children = ( + 3592E88D2C2ED5B200D7F91D /* CustomerCenterConfigResponse.swift */, 5774F9B52805E6CC00997128 /* CustomerInfoResponse.swift */, 5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */, 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */, @@ -3356,6 +3464,7 @@ 887A60652C1D037000E1A461 /* RevenueCatUI */ = { isa = PBXGroup; children = ( + 353756642C382C2800A1B8D6 /* CustomerCenter */, 887A62242C1D168B00E1A461 /* RevenueCatUITests */, 887A5FDD2C1D037000E1A461 /* Data */, 887A5FE72C1D037000E1A461 /* Helpers */, @@ -3527,6 +3636,7 @@ 5766AAAF283D8CDC00FA6091 /* CacheFetchPolicy.swift */, B34605A3279A6E380031CA74 /* CallbackCache.swift */, B34605A4279A6E380031CA74 /* CallbackCacheStatus.swift */, + 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */, B34605A7279A6E380031CA74 /* CustomerInfoCallback.swift */, B34605A6279A6E380031CA74 /* LogInCallback.swift */, B34605A5279A6E380031CA74 /* OfferingsCallback.swift */, @@ -3539,6 +3649,7 @@ isa = PBXGroup; children = ( B34605AE279A6E380031CA74 /* Handling */, + 3592E88B2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift */, B34605B5279A6E380031CA74 /* GetCustomerInfoOperation.swift */, B34605B4279A6E380031CA74 /* GetIntroEligibilityOperation.swift */, B34605BA279A6E380031CA74 /* GetOfferingsOperation.swift */, @@ -4492,18 +4603,20 @@ 2DDF419D24F6F331005BC22D /* IntroEligibilityCalculator.swift in Sources */, 57536A2627851FFE00E2AE7F /* SK1StoreTransaction.swift in Sources */, 57DE807128074C23008D6C6F /* SK1Storefront.swift in Sources */, + 3592E88C2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift in Sources */, + 3592E88E2C2ED5B200D7F91D /* CustomerCenterConfigResponse.swift in Sources */, 578D79742936A36B0042E434 /* LoggerType.swift in Sources */, B34605EB279A766C0031CA74 /* OperationQueue+Extensions.swift in Sources */, 57E6C2C72975AAE1001AFE98 /* FileReader.swift in Sources */, 4F7DBFBD2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift in Sources */, 5766AB4728401B8400FA6091 /* PackageType.swift in Sources */, - 358C756C2C332BE800ECCA12 /* PreferredLocalesProvider.swift in Sources */, B3F3E8DA277158FE0047A5B9 /* DNSChecker.swift in Sources */, A525BF4B26C320D100C354C4 /* SubscriberAttributesManager.swift in Sources */, 2D1015DA275959840086173F /* StoreTransaction.swift in Sources */, 4DBF1F362B4D572400D52354 /* LocalReceiptFetcher.swift in Sources */, 57488A7F29CA145B0000EE7E /* ProductEntitlementMappingResponse.swift in Sources */, B34605BC279A6E380031CA74 /* CallbackCache.swift in Sources */, + 3592E8862C2ED51700D7F91D /* CustomerCenterConfigCallback.swift in Sources */, B3E26A4A26BE0A8E003ACCF3 /* Error+Extensions.swift in Sources */, 57EAE52D274468900060EB74 /* RawDataContainer.swift in Sources */, B35042C426CDB79A00905B95 /* Purchases.swift in Sources */, @@ -4516,6 +4629,7 @@ 579415D529368AB200218FBC /* ReceiptStrings.swift in Sources */, 57A0FBF02749C0C2009E2FC3 /* Atomic.swift in Sources */, 4F98E9D32A465A4400DB6EAB /* TestStoreProduct.swift in Sources */, + 3592E8902C2ED5C100D7F91D /* CustomerCenterConfigAPI.swift in Sources */, 2DC5623224EC63730031F69B /* TransactionsFactory.swift in Sources */, 579415D2293689DD00218FBC /* Codable+Extensions.swift in Sources */, 2DDF41B424F6F387005BC22D /* ASN1ContainerBuilder.swift in Sources */, @@ -4538,6 +4652,7 @@ 9A65E0762591977200DE00B0 /* IdentityStrings.swift in Sources */, 4F6ABC782A81595900250E63 /* PaywallCacheWarming.swift in Sources */, F5714EAA26D7A85D00635477 /* PeriodType+Extensions.swift in Sources */, + 3592E88A2C2ED54A00D7F91D /* CustomerCenterConfigData.swift in Sources */, 57045B3A29C51751001A5417 /* GetProductEntitlementMappingOperation.swift in Sources */, 4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */, F5BE447D269E4ADB00254A30 /* ASIdManagerProxy.swift in Sources */, @@ -4597,6 +4712,7 @@ 4F8038332A1EA7C300D21039 /* TransactionPoster.swift in Sources */, 4F6E81E62A82AAE1006EF181 /* HTTPRequestPath.swift in Sources */, B32B74FF26868AEB005647BF /* Package.swift in Sources */, + 353756522C382BC700A1B8D6 /* PreferredLocalesProvider.swift in Sources */, 35D159CF2BC43B89004D8061 /* DiagnosticsSynchronizer.swift in Sources */, 578DAA482948EEAD001700FD /* Clock.swift in Sources */, 2DDF41B324F6F387005BC22D /* InAppPurchaseBuilder.swift in Sources */, @@ -4733,11 +4849,13 @@ 351B517426D44F4B00BD2BD7 /* MockPaymentDiscount.swift in Sources */, 57488B8B29CB756A0000EE7E /* ProductEntitlementMappingTests.swift in Sources */, 57ACB12428174B9F000DCC9F /* CustomerInfo+TestExtensions.swift in Sources */, + 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */, 351B51B926D450E800BD2BD7 /* TransactionsFactoryTests.swift in Sources */, 351B51BB26D450E800BD2BD7 /* ProductRequestDataInitializationTests.swift in Sources */, 351B515426D44B0A00BD2BD7 /* MockStoreKit1Wrapper.swift in Sources */, 4F0BBAAC2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift in Sources */, 35F8B8FA26E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift in Sources */, + 35F38B482C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift in Sources */, 35D83300262FAD8000E60AC5 /* ETagManagerTests.swift in Sources */, 57DDA7B329CBEFB30098B89D /* MockPurchasedProductsFetcher.swift in Sources */, 5796A39027D6BCD100653165 /* BackendGetIntroEligibilityTests.swift in Sources */, @@ -5055,26 +5173,36 @@ 887A60C92C1D037000E1A461 /* PurchaseButton.swift in Sources */, 887A60812C1D037000E1A461 /* PaywallData+Default.swift in Sources */, 887A606A2C1D037000E1A461 /* TrialOrIntroEligibilityChecker.swift in Sources */, + 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */, 887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */, 887A60C02C1D037000E1A461 /* AsyncButton.swift in Sources */, 887A60892C1D037000E1A461 /* PaywallPurchasesType.swift in Sources */, + 3537566F2C382C2800A1B8D6 /* WrongPlatformView.swift in Sources */, 887A60C22C1D037000E1A461 /* ErrorDisplay.swift in Sources */, + 3537566E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift in Sources */, 887A60692C1D037000E1A461 /* IntroEligibilityViewModel.swift in Sources */, 88A543E32C37A4970039C6A5 /* Template7View.swift in Sources */, 887A60CB2C1D037000E1A461 /* TemplateBackgroundImageView.swift in Sources */, + 353756682C382C2800A1B8D6 /* CustomerCenterViewModel.swift in Sources */, 887A60BD2C1D037000E1A461 /* TemplateViewType.swift in Sources */, 887A606D2C1D037000E1A461 /* Localization.swift in Sources */, 887A60CA2C1D037000E1A461 /* RemoteImage.swift in Sources */, 887A607B2C1D037000E1A461 /* Bundle+Extensions.swift in Sources */, + 353756662C382C2800A1B8D6 /* CustomerCenterError.swift in Sources */, 887A60CF2C1D037000E1A461 /* View+PresentPaywallFooter.swift in Sources */, + 3537566B2C382C2800A1B8D6 /* CustomerCenterView.swift in Sources */, 887A60BB2C1D037000E1A461 /* Template4View.swift in Sources */, 887A607E2C1D037000E1A461 /* Logger.swift in Sources */, 887A60852C1D037000E1A461 /* FitToAspectRatio.swift in Sources */, + 353756652C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift in Sources */, + 353756692C382C2800A1B8D6 /* CustomerCenterViewState.swift in Sources */, 887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */, + 353756672C382C2800A1B8D6 /* SubscriptionInformation.swift in Sources */, 887A60682C1D037000E1A461 /* TemplateError.swift in Sources */, 887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */, 887A60742C1D037000E1A461 /* Strings.swift in Sources */, 887A60712C1D037000E1A461 /* PaywallViewConfiguration.swift in Sources */, + 3537566C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift in Sources */, 887A60C82C1D037000E1A461 /* ProgressView.swift in Sources */, 887A60D02C1D037000E1A461 /* View+PurchaseRestoreCompleted.swift in Sources */, 887A60CD2C1D037000E1A461 /* PaywallView.swift in Sources */, @@ -5088,14 +5216,18 @@ 887A60CE2C1D037000E1A461 /* View+PresentPaywall.swift in Sources */, 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, + 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, 887A60BA2C1D037000E1A461 /* Template3View.swift in Sources */, 887A607D2C1D037000E1A461 /* ImageLoader.swift in Sources */, 887A60822C1D037000E1A461 /* PreviewHelpers.swift in Sources */, 887A60B92C1D037000E1A461 /* Template2View.swift in Sources */, + 3537566D2C382C2800A1B8D6 /* NoSubscriptionsView.swift in Sources */, 887A606B2C1D037000E1A461 /* TrialOrIntroEligibilityChecker+TestData.swift in Sources */, 887A606C2C1D037000E1A461 /* Constants.swift in Sources */, 887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */, + 353756702C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift in Sources */, 887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */, + 3537566A2C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift in Sources */, 887A60842C1D037000E1A461 /* ConsistentPackageContentView.swift in Sources */, 887A60C42C1D037000E1A461 /* IconView.swift in Sources */, 887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift deleted file mode 100644 index 12685ef769..0000000000 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// 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 -// -// CustomerCenterConfigData.swift -// -// -// Created by Cesar de la Vega on 28/5/24. -// - -import Foundation -import RevenueCat - -struct CustomerCenterConfigData { - - let id: String - let paths: [HelpPath] - let title: String - - enum HelpPathType: String { - case missingPurchase = "MISSING_PURCHASE" - case refundRequest = "REFUND_REQUEST" - case changePlans = "CHANGE_PLANS" - case cancel = "CANCEL" - case unknown - } - - enum HelpPathDetail { - - case promotionalOffer(PromotionalOffer) - case feedbackSurvey(FeedbackSurvey) - - } - - struct HelpPath { - - let id: String - let title: String - let type: HelpPathType - let detail: HelpPathDetail? - - } - - struct FeedbackSurvey { - - let title: String - let options: [FeedbackSurveyOption] - - } - - struct FeedbackSurveyOption { - - let id: String - let title: String - - } - -} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 57b50e2d93..7162901f85 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -20,50 +20,88 @@ enum CustomerCenterConfigTestData { @available(iOS 14.0, *) static let customerCenterData = CustomerCenterConfigData( - id: "customer_center_id", - paths: [ - .init( - id: "1", - title: "Didn't receive purchase", - type: .missingPurchase, - detail: nil - ), - .init( - id: "2", - title: "Request a refund", - type: .refundRequest, - detail: nil - ), - .init( - id: "3", - title: "Change plans", - type: .changePlans, - detail: nil - ), - .init( - id: "4", - title: "Cancel subscription", - type: .cancel, - detail: .feedbackSurvey(.init( - title: "Why are you cancelling?", - options: [ + screens: [.management: + .init( + type: .management, + title: "Manage Subscription", + subtitle: "Manage your subscription details here", + paths: [ .init( id: "1", - title: "Too expensive" + title: "Didn't receive purchase", + type: .missingPurchase, + detail: nil ), .init( id: "2", - title: "Don't use the app" + title: "Request a refund", + type: .refundRequest, + detail: nil ), .init( id: "3", - title: "Bought by mistake" + title: "Change plans", + type: .changePlans, + detail: nil + ), + .init( + id: "4", + title: "Cancel subscription", + type: .cancel, + detail: .feedbackSurvey(.init( + title: "Why are you cancelling?", + options: [ + .init( + id: "1", + title: "Too expensive" + ), + .init( + id: "2", + title: "Don't use the app" + ), + .init( + id: "3", + title: "Bought by mistake" + ) + ] + )) ) ] - )) - ) + ), + .noActive: .init( + type: .noActive, + title: "No Active Subscription", + subtitle: "You currently have no active subscriptions", + paths: [ + .init( + id: "9q9719171o", + title: "Check purchases", + type: .missingPurchase, + detail: nil + ) + ] + ) ], - title: "How can we help?" + appearance: .init( + mode: .custom, + light: .init( + accentColor: "#ffffff", + backgroundColor: "#000000", + textColor: "#000000" + ), + dark: .init( + accentColor: "#000000", + backgroundColor: "#ffffff", + textColor: "#ffffff" + ) + ), + localization: .init( + locale: "en_US", + localizedStrings: [ + "cancel": "Cancel", + "back": "Back" + ] + ) ) static let subscriptionInformation: SubscriptionInformation = .init( diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 1be0d23a0d..2a7a47ddb8 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -38,9 +38,11 @@ import RevenueCat } } } + @Published + var configuration: CustomerCenterConfigData? var isLoaded: Bool { - return state != .notLoaded + return state != .notLoaded && configuration != nil } private var customerInfoFetcher: CustomerInfoFetcher @@ -97,4 +99,12 @@ import RevenueCat } } + func loadCustomerCenterConfig() async { + do { + self.configuration = try await Purchases.shared.loadCustomerCenter() + } catch { + self.state = .error(error) + } + } + } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 57f14dd358..fa569ecadd 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -25,12 +25,8 @@ import RevenueCat @MainActor class ManageSubscriptionsViewModel: ObservableObject { - @Published - private(set) var subscriptionInformation: SubscriptionInformation? - @Published - private(set) var refundRequestStatusMessage: String? - @Published - private(set) var configuration: CustomerCenterConfigData? + let screen: CustomerCenterConfigData.Screen + @Published var showRestoreAlert: Bool = false @Published @@ -41,29 +37,34 @@ class ManageSubscriptionsViewModel: ObservableObject { } } } - var isLoaded: Bool { return state != .notLoaded } + @Published + private(set) var subscriptionInformation: SubscriptionInformation? + @Published + private(set) var refundRequestStatusMessage: String? + private var purchasesProvider: ManageSubscriptionsPurchaseType private var error: Error? - convenience init() { - self.init(purchasesProvider: ManageSubscriptionPurchases()) + convenience init(screen: CustomerCenterConfigData.Screen) { + self.init(screen: screen, + purchasesProvider: ManageSubscriptionPurchases()) } - // @PublicForExternalTesting - init(purchasesProvider: ManageSubscriptionsPurchaseType) { + init(screen: CustomerCenterConfigData.Screen, + purchasesProvider: ManageSubscriptionsPurchaseType) { self.state = .notLoaded + self.screen = screen self.purchasesProvider = purchasesProvider } - // @PublicForExternalTesting - init(configuration: CustomerCenterConfigData, + init(screen: CustomerCenterConfigData.Screen, subscriptionInformation: SubscriptionInformation) { - self.configuration = configuration + self.screen = screen self.subscriptionInformation = subscriptionInformation self.purchasesProvider = ManageSubscriptionPurchases() state = .success @@ -72,7 +73,6 @@ class ManageSubscriptionsViewModel: ObservableObject { func loadScreen() async { do { try await loadSubscriptionInformation() - loadCustomerCenterConfig() self.state = .success } catch { self.state = .error(error) @@ -104,10 +104,6 @@ class ManageSubscriptionsViewModel: ObservableObject { ) } - private func loadCustomerCenterConfig() { - self.configuration = CustomerCenterConfigTestData.customerCenterData - } - #if os(iOS) || targetEnvironment(macCatalyst) func handleAction(for path: CustomerCenterConfigData.HelpPath) async { switch path.type { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index d7c843bfbf..853cd5ca8c 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -41,11 +41,13 @@ public struct CustomerCenterView: View { if !viewModel.isLoaded { ProgressView() } else { - destinationView() + if let configuration = viewModel.configuration { + destinationView(configuration: configuration) + } } } .task { - await checkAndLoadSubscriptions() + await loadInformationIfNeeded() } } @@ -58,22 +60,24 @@ public struct CustomerCenterView: View { @available(visionOS, unavailable) private extension CustomerCenterView { - func checkAndLoadSubscriptions() async { + func loadInformationIfNeeded() async { if !viewModel.isLoaded { await viewModel.loadHasSubscriptions() + await viewModel.loadCustomerCenterConfig() } } @ViewBuilder - func destinationView() -> some View { + func destinationView(configuration: CustomerCenterConfigData) -> some View { if viewModel.hasSubscriptions { - if viewModel.subscriptionsAreFromApple { - ManageSubscriptionsView() + if viewModel.subscriptionsAreFromApple, + let screen = configuration.screens[.management] { + ManageSubscriptionsView(screen: screen) } else { WrongPlatformView() } } else { - NoSubscriptionsView() + NoSubscriptionsView(configuration: configuration) } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 08869af465..4aee4f7911 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -29,9 +29,12 @@ struct ManageSubscriptionsView: View { var openURL @StateObject - private var viewModel = ManageSubscriptionsViewModel() + private var viewModel: ManageSubscriptionsViewModel - init() { } + init(screen: CustomerCenterConfigData.Screen) { + let viewModel = ManageSubscriptionsViewModel(screen: screen) + self._viewModel = .init(wrappedValue: viewModel) + } fileprivate init(viewModel: ManageSubscriptionsViewModel) { self._viewModel = .init(wrappedValue: viewModel) @@ -39,17 +42,17 @@ struct ManageSubscriptionsView: View { var body: some View { VStack { - if viewModel.isLoaded { - HeaderView(viewModel: viewModel) + if self.viewModel.isLoaded { + HeaderView(viewModel: self.viewModel) if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - refundRequestStatusMessage: viewModel.refundRequestStatusMessage) + refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) } Spacer() - ManageSubscriptionsButtonsView(viewModel: viewModel) + ManageSubscriptionsButtonsView(viewModel: self.viewModel) } else { ProgressView() .progressViewStyle(CircularProgressViewStyle()) @@ -70,7 +73,7 @@ struct ManageSubscriptionsView: View { private extension ManageSubscriptionsView { func loadInformationIfNeeded() async { - if !viewModel.isLoaded { + if !self.viewModel.isLoaded { await viewModel.loadScreen() } } @@ -88,11 +91,9 @@ struct HeaderView: View { private(set) var viewModel: ManageSubscriptionsViewModel var body: some View { - if let configuration = viewModel.configuration { - Text(configuration.title) - .font(.title) - .padding() - } + Text(self.viewModel.screen.title) + .font(.title) + .padding() } } @@ -148,23 +149,21 @@ struct ManageSubscriptionsButtonsView: View { var body: some View { VStack(spacing: 16) { - if let configuration = viewModel.configuration { - let filteredPaths = configuration.paths.filter { path in - #if targetEnvironment(macCatalyst) - return path.type == .refundRequest - #else - return true - #endif - } - ForEach(filteredPaths, id: \.id) { path in - AsyncButton(action: { - await self.viewModel.handleAction(for: path) - }, label: { - Text(path.title) - }) - .restorePurchasesAlert(isPresented: $viewModel.showRestoreAlert) - .buttonStyle(ManageSubscriptionsButtonStyle()) - } + let filteredPaths = self.viewModel.screen.paths.filter { path in + #if targetEnvironment(macCatalyst) + return path.type == .refundRequest + #else + return true + #endif + } + ForEach(filteredPaths, id: \.id) { path in + AsyncButton(action: { + await self.viewModel.handleAction(for: path) + }, label: { + Text(path.title) + }) + .restorePurchasesAlert(isPresented: self.$viewModel.showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) } } } @@ -182,7 +181,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { let viewModel = ManageSubscriptionsViewModel( - configuration: CustomerCenterConfigTestData.customerCenterData, + screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformation) ManageSubscriptionsView(viewModel: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 58cecf658f..2790e182f5 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -25,8 +25,19 @@ import SwiftUI @available(visionOS, unavailable) struct NoSubscriptionsView: View { - @Environment(\.dismiss) var dismiss - @State private var showRestoreAlert: Bool = false + // swiftlint:disable:next todo + // TODO: build screen using this configuration + let configuration: CustomerCenterConfigData + + @Environment(\.dismiss) + var dismiss + + @State + private var showRestoreAlert: Bool = false + + init(configuration: CustomerCenterConfigData) { + self.configuration = configuration + } var body: some View { VStack { @@ -65,7 +76,7 @@ struct NoSubscriptionsView: View { struct NoSubscriptionsView_Previews: PreviewProvider { static var previews: some View { - NoSubscriptionsView() + NoSubscriptionsView(configuration: CustomerCenterConfigTestData.customerCenterData) } } diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 5f9e81ddad..dc8beb6a04 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -43,7 +43,7 @@ enum Strings { case executing_external_purchase_logic case executing_restore_logic case executing_external_restore_logic - + case could_not_find_subscription_information } @@ -119,7 +119,7 @@ extension Strings: CustomStringConvertible { return "Will execute custom StoreKit restore purchases logic provided by your app. " + "No StoreKit restore purchases logic will be performed by RevenueCat. " + "You must have initialized your `PaywallView` appropriately." - + case .could_not_find_subscription_information: return "Could not find any active subscription's information" } diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift new file mode 100644 index 0000000000..6ee8ddd8b0 --- /dev/null +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -0,0 +1,313 @@ +// +// 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 +// +// CustomerCenterConfigData.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +// swiftlint:disable missing_docs +// swiftlint:disable nesting +public struct CustomerCenterConfigData { + + public let screens: [Screen.ScreenType: Screen] + public let appearance: Appearance + public let localization: Localization + + public init(screens: [Screen.ScreenType: Screen], appearance: Appearance, localization: Localization) { + self.screens = screens + self.appearance = appearance + self.localization = localization + } + + public struct Localization { + + let locale: String + let localizedStrings: [String: String] + + public init(locale: String, localizedStrings: [String: String]) { + self.locale = locale + self.localizedStrings = localizedStrings + } + + } + + public struct HelpPath { + + public let id: String + public let title: String + public let type: PathType + public let detail: PathDetail? + + public init(id: String, + title: String, + type: PathType, + detail: PathDetail?) { + self.id = id + self.title = title + self.type = type + self.detail = detail + } + + public enum PathDetail { + + case promotionalOffer(PromotionalOffer) + case feedbackSurvey(FeedbackSurvey) + + } + + public enum PathType: String { + + case missingPurchase = "MISSING_PURCHASE" + case refundRequest = "REFUND_REQUEST" + case changePlans = "CHANGE_PLANS" + case cancel = "CANCEL" + case unknown + + init(from rawValue: String) { + switch rawValue { + case "MISSING_PURCHASE": + self = .missingPurchase + case "REFUND_REQUEST": + self = .refundRequest + case "CHANGE_PLANS": + self = .changePlans + case "CANCEL": + self = .cancel + default: + self = .unknown + } + } + + } + + public struct PromotionalOffer { + + public let iosOfferId: String + public let eligible: Bool + + public init(iosOfferId: String, eligible: Bool) { + self.iosOfferId = iosOfferId + self.eligible = eligible + } + + } + + public struct FeedbackSurvey { + + public let title: String + public let options: [Option] + + public init(title: String, options: [Option]) { + self.title = title + self.options = options + } + + public struct Option { + + public let id: String + public let title: String + + public init(id: String, title: String) { + self.id = id + self.title = title + } + + } + + } + + } + + public struct Appearance { + + let mode: AppearanceMode + let light: AppearanceCustomColors + let dark: AppearanceCustomColors + + public init(mode: AppearanceMode, light: AppearanceCustomColors, dark: AppearanceCustomColors) { + self.mode = mode + self.light = light + self.dark = dark + } + + public struct AppearanceCustomColors { + + let accentColor: String + let backgroundColor: String + let textColor: String + + public init(accentColor: String, backgroundColor: String, textColor: String) { + self.accentColor = accentColor + self.backgroundColor = backgroundColor + self.textColor = textColor + } + + } + + public enum AppearanceMode: String { + + case custom = "CUSTOM" + case system = "SYSTEM" + + init(from rawValue: String) { + switch rawValue { + case "CUSTOM": + self = .custom + case "SYSTEM": + self = .system + default: + self = .system + } + } + + } + + } + + public struct Screen { + + public let type: ScreenType + public let title: String + public let subtitle: String? + public let paths: [HelpPath] + + public init(type: ScreenType, title: String, subtitle: String?, paths: [HelpPath]) { + self.type = type + self.title = title + self.subtitle = subtitle + self.paths = paths + } + + public enum ScreenType: String { + case management = "MANAGEMENT" + case noActive = "NO_ACTIVE" + case unknown + + init(from rawValue: String) { + switch rawValue { + case "MANAGEMENT": + self = .management + case "NO_ACTIVE": + self = .noActive + default: + self = .unknown + } + } + } + + } + +} + +extension CustomerCenterConfigData { + + init(from response: CustomerCenterConfigResponse) { + let localization = Localization(from: response.customerCenter.localization) + self.localization = localization + self.appearance = Appearance(from: response.customerCenter.appearance) + self.screens = Dictionary(uniqueKeysWithValues: response.customerCenter.screens.map { + let type = CustomerCenterConfigData.Screen.ScreenType(from: $0.key) + return (type, Screen(from: $0.value, localization: localization)) + }) + } + +} + +extension CustomerCenterConfigData.Screen { + + init(from response: CustomerCenterConfigResponse.Screen, + localization: CustomerCenterConfigData.Localization) { + self.type = ScreenType(from: response.type.rawValue) + self.title = response.title + self.subtitle = response.subtitle + self.paths = response.paths.map { CustomerCenterConfigData.HelpPath(from: $0) } + } + +} + +extension CustomerCenterConfigData.Appearance { + + init(from response: CustomerCenterConfigResponse.Appearance) { + self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response.mode) + self.light = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.light) + self.dark = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.dark) + } + +} + +extension CustomerCenterConfigData.Appearance.AppearanceCustomColors { + + init(from response: CustomerCenterConfigResponse.Appearance.AppearanceCustomColors) { + // swiftlint:disable:next todo + // TODO: convert colors to PaywallColor (RCColor) + self.accentColor = response.accentColor + self.backgroundColor = response.backgroundColor + self.textColor = response.textColor + } + +} + +extension CustomerCenterConfigData.Localization { + + init(from response: CustomerCenterConfigResponse.Localization) { + // swiftlint:disable:next todo + // TODO: convert to Locale + self.locale = response.locale + self.localizedStrings = response.localizedStrings + } + +} + +extension CustomerCenterConfigData.HelpPath { + + init(from response: CustomerCenterConfigResponse.HelpPath) { + self.id = response.id + self.title = response.title + self.type = CustomerCenterConfigData.HelpPath.PathType(from: response.type.rawValue) + if let promotionalOfferResponse = response.promotionalOffer { + self.detail = .promotionalOffer(PromotionalOffer(from: promotionalOfferResponse)) + } else if let feedbackSurveyResponse = response.feedbackSurvey { + self.detail = .feedbackSurvey(FeedbackSurvey(from: feedbackSurveyResponse)) + } else { + self.detail = nil + } + } + +} + +extension CustomerCenterConfigData.HelpPath.PromotionalOffer { + + init(from response: CustomerCenterConfigResponse.HelpPath.PromotionalOffer) { + self.iosOfferId = response.iosOfferId + self.eligible = response.eligible + } + +} + +extension CustomerCenterConfigData.HelpPath.FeedbackSurvey { + + init(from response: CustomerCenterConfigResponse.HelpPath.FeedbackSurvey) { + self.title = response.title + self.options = response.options.map { Option(from: $0) } + } + +} + +extension CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option { + + init(from response: CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option) { + self.id = response.id + self.title = response.title + } + +} diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index 2faffcd329..9f2b6d03af 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -20,6 +20,7 @@ class Backend { let offlineEntitlements: OfflineEntitlementsAPI let customer: CustomerAPI let internalAPI: InternalAPI + let customerCenterConfig: CustomerCenterConfigAPI private let config: BackendConfiguration @@ -56,13 +57,15 @@ class Backend { let offerings = OfferingsAPI(backendConfig: backendConfig) let offlineEntitlements = OfflineEntitlementsAPI(backendConfig: backendConfig) let internalAPI = InternalAPI(backendConfig: backendConfig) + let customerCenterConfig = CustomerCenterConfigAPI(backendConfig: backendConfig) self.init(backendConfig: backendConfig, customerAPI: customer, identityAPI: identity, offeringsAPI: offerings, offlineEntitlements: offlineEntitlements, - internalAPI: internalAPI) + internalAPI: internalAPI, + customerCenterConfig: customerCenterConfig) } required init(backendConfig: BackendConfiguration, @@ -70,7 +73,8 @@ class Backend { identityAPI: IdentityAPI, offeringsAPI: OfferingsAPI, offlineEntitlements: OfflineEntitlementsAPI, - internalAPI: InternalAPI) { + internalAPI: InternalAPI, + customerCenterConfig: CustomerCenterConfigAPI) { self.config = backendConfig self.customer = customerAPI @@ -78,6 +82,7 @@ class Backend { self.offerings = offeringsAPI self.offlineEntitlements = offlineEntitlements self.internalAPI = internalAPI + self.customerCenterConfig = customerCenterConfig } func clearHTTPClientCaches() { diff --git a/Sources/Networking/Caching/CustomerCenterConfigCallback.swift b/Sources/Networking/Caching/CustomerCenterConfigCallback.swift new file mode 100644 index 0000000000..9ac24785b8 --- /dev/null +++ b/Sources/Networking/Caching/CustomerCenterConfigCallback.swift @@ -0,0 +1,23 @@ +// +// 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 +// +// CustomerCenterConfigCallback.swift +// +// +// Created by Cesar de la Vega on 31/5/24. +// + +import Foundation + +struct CustomerCenterConfigCallback: CacheKeyProviding { + + let cacheKey: String + let completion: (Result) -> Void + +} diff --git a/Sources/Networking/CustomerCenterConfigAPI.swift b/Sources/Networking/CustomerCenterConfigAPI.swift new file mode 100644 index 0000000000..a46b42430f --- /dev/null +++ b/Sources/Networking/CustomerCenterConfigAPI.swift @@ -0,0 +1,50 @@ +// +// 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 +// +// CustomerCenterConfigAPI.swift +// +// Created by Cesar de la Vega on 31/5/24. +// + +import Foundation + +class CustomerCenterConfigAPI { + + typealias CustomerCenterConfigResponseHandler = Backend.ResponseHandler + + private let customerCenterConfigResponseCallbacksCache: CallbackCache + private let backendConfig: BackendConfiguration + + init(backendConfig: BackendConfiguration) { + self.backendConfig = backendConfig + self.customerCenterConfigResponseCallbacksCache = .init() + } + + func getCustomerCenterConfig(appUserID: String, + isAppBackgrounded: Bool, + completion: @escaping CustomerCenterConfigResponseHandler) { + let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient, + appUserID: appUserID) + + let factory = GetCustomerCenterConfigOperation.createFactory( + configuration: config, + callbackCache: self.customerCenterConfigResponseCallbacksCache + ) + + let callback = CustomerCenterConfigCallback(cacheKey: factory.cacheKey, completion: completion) + let cacheStatus = self.customerCenterConfigResponseCallbacksCache.add(callback) + + self.backendConfig.addCacheableOperation( + with: factory, + delay: .default(forBackgroundedApp: isAppBackgrounded), + cacheStatus: cacheStatus + ) + } + +} diff --git a/Sources/Networking/HTTPClient/HTTPRequestPath.swift b/Sources/Networking/HTTPClient/HTTPRequestPath.swift index 636de96bf8..9626b91a82 100644 --- a/Sources/Networking/HTTPClient/HTTPRequestPath.swift +++ b/Sources/Networking/HTTPClient/HTTPRequestPath.swift @@ -70,6 +70,7 @@ extension HTTPRequest { case postAdServicesToken(appUserID: String) case health case getProductEntitlementMapping + case getCustomerCenterConfig(appUserID: String) } @@ -102,7 +103,8 @@ extension HTTPRequest.Path: HTTPRequestPath { .postReceiptData, .postSubscriberAttributes, .postAdServicesToken, - .getProductEntitlementMapping: + .getProductEntitlementMapping, + .getCustomerCenterConfig: return true case .health: @@ -121,7 +123,8 @@ extension HTTPRequest.Path: HTTPRequestPath { .postReceiptData, .postSubscriberAttributes, .postAdServicesToken, - .getProductEntitlementMapping: + .getProductEntitlementMapping, + .getCustomerCenterConfig: return true case .health: return false @@ -141,7 +144,8 @@ extension HTTPRequest.Path: HTTPRequestPath { .postSubscriberAttributes, .postAttributionData, .postAdServicesToken, - .postOfferForSigning: + .postOfferForSigning, + .getCustomerCenterConfig: return false } } @@ -159,7 +163,8 @@ extension HTTPRequest.Path: HTTPRequestPath { .postAttributionData, .postAdServicesToken, .postOfferForSigning, - .getProductEntitlementMapping: + .getProductEntitlementMapping, + .getCustomerCenterConfig: return false } } @@ -199,6 +204,9 @@ extension HTTPRequest.Path: HTTPRequestPath { case .getProductEntitlementMapping: return "product_entitlement_mapping" + case let .getCustomerCenterConfig(appUserID): + return "customercenter/\(Self.escape(appUserID))" + } } @@ -237,6 +245,9 @@ extension HTTPRequest.Path: HTTPRequestPath { case .getProductEntitlementMapping: return "get_product_entitlement_mapping" + case .getCustomerCenterConfig: + return "customer_center" + } } diff --git a/Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift b/Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift new file mode 100644 index 0000000000..605bc5e82a --- /dev/null +++ b/Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift @@ -0,0 +1,87 @@ +// +// 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 +// +// GetCustomerCenterConfigOperation.swift +// +// +// Created by Cesar de la Vega on 31/5/24. +// + +import Foundation + +final class GetCustomerCenterConfigOperation: CacheableNetworkOperation { + + private let customerCenterConfigCallbackCache: CallbackCache + private let configuration: AppUserConfiguration + + static func createFactory( + configuration: UserSpecificConfiguration, + callbackCache: CallbackCache + ) -> CacheableNetworkOperationFactory { + return .init({ cacheKey in + .init( + configuration: configuration, + customerCenterConfigCallbackCache: callbackCache, + cacheKey: cacheKey + ) + }, + individualizedCacheKeyPart: configuration.appUserID) + } + + private init(configuration: UserSpecificConfiguration, + customerCenterConfigCallbackCache: CallbackCache, + cacheKey: String) { + self.configuration = configuration + self.customerCenterConfigCallbackCache = customerCenterConfigCallbackCache + + super.init(configuration: configuration, cacheKey: cacheKey) + } + + override func begin(completion: @escaping () -> Void) { + self.getCustomerCenterConfig(completion: completion) + } + +} + +private extension GetCustomerCenterConfigOperation { + + func getCustomerCenterConfig(completion: @escaping () -> Void) { + let appUserID = self.configuration.appUserID + + guard appUserID.isNotEmpty else { + self.customerCenterConfigCallbackCache.performOnAllItemsAndRemoveFromCache( + withCacheable: self + ) { callback in + callback.completion(.failure(.missingAppUserID())) + } + completion() + + return + } + + let request = HTTPRequest(method: .get, path: .getCustomerCenterConfig(appUserID: appUserID)) + + httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in + defer { + completion() + } + + self.customerCenterConfigCallbackCache.performOnAllItemsAndRemoveFromCache( + withCacheable: self + ) { callback in + callback.completion( + response + .map { $0.body } + .mapError(BackendError.networkError) + ) + } + } + } + +} diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift new file mode 100644 index 0000000000..1241358369 --- /dev/null +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -0,0 +1,137 @@ +// +// 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 +// +// CustomerCenterConfigResponse.swift +// +// +// Created by Cesar de la Vega on 31/5/24. +// + +import Foundation + +// swiftlint:disable nesting + +struct CustomerCenterConfigResponse { + + let customerCenter: CustomerCenter + + struct CustomerCenter { + + let appearance: Appearance + let screens: [String: Screen] + let localization: Localization + let support: Support + + } + + struct Localization { + + let locale: String + let localizedStrings: [String: String] + + } + + struct HelpPath { + + let id: String + let title: String + let type: PathType + let promotionalOffer: PromotionalOffer? + let feedbackSurvey: FeedbackSurvey? + + enum PathType: String { + + case missingPurchase = "MISSING_PURCHASE" + case refundRequest = "REFUND_REQUEST" + case changePlans = "CHANGE_PLANS" + case cancel = "CANCEL" + case unknown + + } + + struct PromotionalOffer { + + let iosOfferId: String + let eligible: Bool + + } + + struct FeedbackSurvey { + + let title: String + let options: [Option] + + struct Option { + + let id: String + let title: String + let promotionalOffer: PromotionalOffer? + + } + + } + + } + + struct Appearance { + + let mode: String + let light: AppearanceCustomColors + let dark: AppearanceCustomColors + + struct AppearanceCustomColors { + + let accentColor: String + let backgroundColor: String + let textColor: String + + } + + } + + struct Screen { + + let title: String + let type: ScreenType + let subtitle: String? + let paths: [HelpPath] + + enum ScreenType: String { + + case management = "MANAGEMENT" + case noActive = "NO_ACTIVE" + case unknown + + } + + } + + struct Support { + + let email: String + + } + +} + +extension CustomerCenterConfigResponse: Codable, Equatable {} +extension CustomerCenterConfigResponse.CustomerCenter: Codable, Equatable {} +extension CustomerCenterConfigResponse.Localization: Codable, Equatable {} +extension CustomerCenterConfigResponse.HelpPath: Codable, Equatable {} +extension CustomerCenterConfigResponse.HelpPath.PathType: Codable, Equatable {} +extension CustomerCenterConfigResponse.HelpPath.PromotionalOffer: Codable, Equatable {} +extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey: Codable, Equatable {} +extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option: Codable, Equatable {} +extension CustomerCenterConfigResponse.Appearance: Codable, Equatable {} +extension CustomerCenterConfigResponse.Appearance.AppearanceCustomColors: Codable, Equatable {} +extension CustomerCenterConfigResponse.Screen: Codable, Equatable {} +extension CustomerCenterConfigResponse.Screen.ScreenType: Codable, Equatable {} +extension CustomerCenterConfigResponse.Support: Codable, Equatable {} + +extension CustomerCenterConfigResponse: HTTPResponseBody {} diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index ae6b393f0e..2997493488 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1189,6 +1189,18 @@ public extension Purchases { await self.paywallEventsManager?.track(paywallEvent: paywallEvent) } + /// Used by `RevenueCatUI` to download customer center data + func loadCustomerCenter() async throws -> CustomerCenterConfigData { + let response = try await Async.call { completion in + self.backend.customerCenterConfig.getCustomerCenterConfig(appUserID: self.appUserID, + isAppBackgrounded: false) { result in + completion(result.mapError(\.asPublicError)) + } + } + + return CustomerCenterConfigData(from: response) + } + /// Used by `RevenueCatUI` to download and cache paywall images. @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *) static let paywallImageDownloadSession: URLSession = PaywallCacheWarming.downloadSession diff --git a/Sources/Purchasing/StoreKitAbstractions/StorefrontProvider.swift b/Sources/Purchasing/StoreKitAbstractions/StorefrontProvider.swift index ba4e3fbd89..030facdef9 100644 --- a/Sources/Purchasing/StoreKitAbstractions/StorefrontProvider.swift +++ b/Sources/Purchasing/StoreKitAbstractions/StorefrontProvider.swift @@ -20,7 +20,7 @@ protocol StorefrontProviderType { } -/// Main `StorefrontProviderType` implementation. +/// Main ``StorefrontProviderType`` implementation. /// Relies on StoreKit 1 because StoreKit 2's implementation would be `async`. final class DefaultStorefrontProvider: StorefrontProviderType { diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj index d9c01c20bd..08790f97dd 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 2DD778EE270E23460079CBD4 /* EntitlementInfosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C526EBE7EA007DDB75 /* EntitlementInfosAPI.swift */; }; 2DD778EF270E23460079CBD4 /* EntitlementInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C826EBE7EA007DDB75 /* EntitlementInfoAPI.swift */; }; 4D30B3B32B32F8FA00B5C7D7 /* StoreKitVersionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D30B3B22B32F8FA00B5C7D7 /* StoreKitVersionAPI.swift */; }; + 35F38B462C3008E500CD29FD /* CustomerCenterConfigDataAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F38B452C3008E500CD29FD /* CustomerCenterConfigDataAPI.swift */; }; 4F1428A22A4A11D7006CD196 /* TestStoreProductAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1428A12A4A11D7006CD196 /* TestStoreProductAPI.swift */; }; 4F1428A72A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1428A62A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift */; }; 4F6BEE752A27C77C00CD9322 /* OtherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */; }; @@ -62,6 +63,7 @@ 2CA538392B7D42100037D96F /* PresentedOfferingContextAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentedOfferingContextAPI.swift; sourceTree = ""; }; 2DD778D0270E233F0079CBD4 /* SwiftAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4D30B3B22B32F8FA00B5C7D7 /* StoreKitVersionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitVersionAPI.swift; sourceTree = ""; }; + 35F38B452C3008E500CD29FD /* CustomerCenterConfigDataAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataAPI.swift; sourceTree = ""; }; 4F1428A12A4A11D7006CD196 /* TestStoreProductAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductAPI.swift; sourceTree = ""; }; 4F1428A62A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductDiscountAPI.swift; sourceTree = ""; }; 4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAPI.swift; sourceTree = ""; }; @@ -169,6 +171,7 @@ A5D614CB26EBE7EA007DDB75 /* TransactionAPI.swift */, 5740FCD42996D7B800E049F9 /* VerificationResultAPI.swift */, 4D30B3B22B32F8FA00B5C7D7 /* StoreKitVersionAPI.swift */, + 35F38B452C3008E500CD29FD /* CustomerCenterConfigDataAPI.swift */, ); path = SwiftAPITester; sourceTree = ""; @@ -262,6 +265,7 @@ 2DD778E9270E23460079CBD4 /* CustomerInfoAPI.swift in Sources */, 5758EE4F2786493400B3B703 /* StoreProductAPI.swift in Sources */, 5753ED9D294A6F3F00CBAB54 /* PurchasesReceiptParserAPI.swift in Sources */, + 35F38B462C3008E500CD29FD /* CustomerCenterConfigDataAPI.swift in Sources */, 2DD778ED270E23460079CBD4 /* OfferingAPI.swift in Sources */, B378153A2857A609000A7B93 /* AttributionAPI.swift in Sources */, 2DD778EE270E23460079CBD4 /* EntitlementInfosAPI.swift in Sources */, diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift new file mode 100644 index 0000000000..22ab3102b6 --- /dev/null +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift @@ -0,0 +1,85 @@ +// +// CustomerCenterConfigDataAPI.swift +// SwiftAPITester +// +// Created by Cesar de la Vega on 29/6/24. +// + +import Foundation +import RevenueCat + +func checkCustomerCenterConfigData(_ data: CustomerCenterConfigData) { + let screens: [CustomerCenterConfigData.Screen.ScreenType: CustomerCenterConfigData.Screen] = data.screens + let appearance: CustomerCenterConfigData.Appearance = data.appearance + let localization: CustomerCenterConfigData.Localization = data.localization + + let _: CustomerCenterConfigData = .init(screens: screens, appearance: appearance, localization: localization) +} + +func checkHelpPath(_ path: CustomerCenterConfigData.HelpPath) { + let id: String = path.id + let title: String = path.title + let type: CustomerCenterConfigData.HelpPath.PathType = path.type + let detail: CustomerCenterConfigData.HelpPath.PathDetail? = path.detail + + let _: CustomerCenterConfigData.HelpPath = .init(id: id, title: title, type: type, detail: detail) +} + +func checkHelpPathDetail(_ detail: CustomerCenterConfigData.HelpPath.PathDetail) { + switch detail { + case .promotionalOffer(let offer): + checkPromotionalOffer(offer) + case .feedbackSurvey(let survey): + checkFeedbackSurvey(survey) + @unknown default: + break + } +} + +func checkPromotionalOffer(_ offer: CustomerCenterConfigData.HelpPath.PromotionalOffer) { + let iosOfferId: String = offer.iosOfferId + let eligible: Bool = offer.eligible + + let _: CustomerCenterConfigData.HelpPath.PromotionalOffer = .init(iosOfferId: iosOfferId, eligible: eligible) +} + +func checkFeedbackSurvey(_ survey: CustomerCenterConfigData.HelpPath.FeedbackSurvey) { + let title: String = survey.title + let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] = survey.options + + let _: CustomerCenterConfigData.HelpPath.FeedbackSurvey = .init(title: title, options: options) +} + +func checkFeedbackSurveyOption(_ option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) { + let id: String = option.id + let title: String = option.title + + let _: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option = .init(id: id, title: title) +} + +func checkScreen(_ screen: CustomerCenterConfigData.Screen) { + let type: CustomerCenterConfigData.Screen.ScreenType = screen.type + let title: String = screen.title + let subtitle: String? = screen.subtitle + let paths: [CustomerCenterConfigData.HelpPath] = screen.paths + + let _: CustomerCenterConfigData.Screen = .init(type: type, title: title, subtitle: subtitle, paths: paths) +} + +func checkScreenType(_ type: CustomerCenterConfigData.Screen.ScreenType) { + switch type { + case .management, .noActive, .unknown: + print(type.rawValue) + @unknown default: + break + } +} + +func checkPathType(_ type: CustomerCenterConfigData.HelpPath.PathType) { + switch type { + case .missingPurchase, .refundRequest, .changePlans, .cancel, .unknown: + print(type.rawValue) + @unknown default: + break + } +} diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 1adb2d6b4d..cb715e048e 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -64,6 +64,7 @@ class CustomerCenterViewModelTests: TestCase { expect(viewModel.isLoaded) == false viewModel.state = .success + viewModel.configuration = CustomerCenterConfigTestData.customerCenterData expect(viewModel.isLoaded) == true } diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 8519697913..b3e29bb134 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -38,20 +38,20 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testInitialState() { - let viewModel = ManageSubscriptionsViewModel() + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) - expect(viewModel.state) == .notLoaded + expect(viewModel.state) == CustomerCenterViewState.notLoaded expect(viewModel.subscriptionInformation).to(beNil()) expect(viewModel.refundRequestStatusMessage).to(beNil()) - expect(viewModel.configuration).to(beNil()) + expect(viewModel.screen).toNot(beNil()) expect(viewModel.showRestoreAlert) == false expect(viewModel.isLoaded) == false } func testStateChangeToError() { - let viewModel = ManageSubscriptionsViewModel() + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) - viewModel.state = .error(error) + viewModel.state = CustomerCenterViewState.error(error) switch viewModel.state { case .error(let stateError): @@ -62,7 +62,7 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testIsLoaded() { - let viewModel = ManageSubscriptionsViewModel() + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) expect(viewModel.isLoaded) == false @@ -72,12 +72,13 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testLoadScreenSuccess() async { - let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases()) + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases()) await viewModel.loadScreen() expect(viewModel.subscriptionInformation).toNot(beNil()) - expect(viewModel.configuration).toNot(beNil()) + expect(viewModel.screen).toNot(beNil()) expect(viewModel.state) == .success expect(viewModel.subscriptionInformation?.title) == "title" @@ -88,8 +89,9 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testLoadScreenNoActiveSubscription() async { - let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( - customerInfo: CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: ManageSubscriptionsViewModelTests.customerInfoWithoutSubscriptions )) await viewModel.loadScreen() @@ -99,7 +101,8 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testLoadScreenFailure() async { - let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases( customerInfoError: error )) @@ -141,14 +144,14 @@ final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { if let customerInfo { return customerInfo } - return CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + return await ManageSubscriptionsViewModelTests.customerInfoWithAppleSubscriptions } func products(_ productIdentifiers: [String]) async -> [RevenueCat.StoreProduct] { if productsShouldFail { return [] } - let product = await CustomerCenterViewModelTests.createMockProduct() + let product = await ManageSubscriptionsViewModelTests.createMockProduct() return [product] } @@ -168,7 +171,10 @@ final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -private extension CustomerCenterViewModelTests { +private extension ManageSubscriptionsViewModelTests { + + static let screen: CustomerCenterConfigData.Screen = + CustomerCenterConfigTestData.customerCenterData.screens[.management]! static func createMockProduct() -> StoreProduct { // Using SK1 products because they can be mocked, but CustomerCenterViewModel diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift new file mode 100644 index 0000000000..b3e58d0b94 --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -0,0 +1,124 @@ +// +// 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 +// +// CustomerCenterConfigDataTests.swift +// +// Created by Cesar de la Vega on 8/7/24. + +import Nimble +import XCTest + +@testable import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +class CustomerCenterConfigDataTests: TestCase { + + func testCustomerCenterConfigDataConversion() throws { + let mockResponse = CustomerCenterConfigResponse( + customerCenter: .init( + appearance: .init( + mode: "CUSTOM", + light: .init(accentColor: "#FFFFFF", backgroundColor: "#000000", textColor: "#FF0000"), + dark: .init(accentColor: "#000000", backgroundColor: "#FFFFFF", textColor: "#00FF00") + ), + screens: [ + "MANAGEMENT": .init( + title: "Management Screen", + type: .management, + subtitle: "Manage your account", + paths: [ + .init( + id: "path1", + title: "Path 1", + type: .missingPurchase, + promotionalOffer: nil, + feedbackSurvey: nil + ), + .init( + id: "path2", + title: "Path 2", + type: .cancel, + promotionalOffer: .init(iosOfferId: "offer_id", eligible: true), + feedbackSurvey: nil + ), + .init( + id: "path3", + title: "Path 3", + type: .changePlans, + promotionalOffer: nil, + feedbackSurvey: .init(title: "survey title", + options: [ + .init(id: "id_1", + title: "option 1", + promotionalOffer: .init(iosOfferId: "offer_id_1", + eligible: true)) + ]) + ) + ] + ) + ], + localization: .init(locale: "en_US", localizedStrings: ["key": "value"]), + support: .init(email: "support@example.com") + ) + ) + + let configData = CustomerCenterConfigData(from: mockResponse) + + expect(configData.localization.locale) == "en_US" + expect(configData.localization.localizedStrings["key"]) == "value" + + expect(configData.appearance.mode.rawValue) == "CUSTOM" + expect(configData.appearance.light.accentColor) == "#FFFFFF" + expect(configData.appearance.light.backgroundColor) == "#000000" + expect(configData.appearance.light.textColor) == "#FF0000" + expect(configData.appearance.dark.accentColor) == "#000000" + expect(configData.appearance.dark.backgroundColor) == "#FFFFFF" + expect(configData.appearance.dark.textColor) == "#00FF00" + + expect(configData.screens.count) == 1 + let managementScreen = try XCTUnwrap(configData.screens[.management]) + expect(managementScreen.type.rawValue) == "MANAGEMENT" + expect(managementScreen.title) == "Management Screen" + expect(managementScreen.subtitle) == "Manage your account" + expect(managementScreen.paths.count) == 3 + + let paths = try XCTUnwrap(managementScreen.paths) + + expect(paths[0].id) == "path1" + expect(paths[0].title) == "Path 1" + expect(paths[0].type.rawValue) == "MISSING_PURCHASE" + expect(paths[0].detail).to(beNil()) + + expect(paths[1].id) == "path2" + expect(paths[1].title) == "Path 2" + expect(paths[1].type.rawValue) == "CANCEL" + if case let .promotionalOffer(promotionalOffer) = paths[1].detail { + expect(promotionalOffer.iosOfferId) == "offer_id" + expect(promotionalOffer.eligible).to(beTrue()) + } else { + fail("Expected promotionalOffer detail") + } + + expect(paths[2].id) == "path3" + expect(paths[2].title) == "Path 3" + expect(paths[2].type.rawValue) == "CHANGE_PLANS" + if case let .feedbackSurvey(feedbackSurvey) = paths[2].detail { + expect(feedbackSurvey.title) == "survey title" + expect(feedbackSurvey.options.count) == 1 + expect(feedbackSurvey.options.first?.id) == "id_1" + expect(feedbackSurvey.options.first?.title) == "option 1" + } else { + fail("Expected feedbackSurvey detail") + } + } + +} diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index 212aec063e..015d490e32 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -33,13 +33,15 @@ class MockBackend: Backend { let offlineEntitlements = MockOfflineEntitlementsAPI() let customer = CustomerAPI(backendConfig: backendConfig, attributionFetcher: attributionFetcher) let internalAPI = InternalAPI(backendConfig: backendConfig) + let customerCenterConfig = CustomerCenterConfigAPI(backendConfig: backendConfig) self.init(backendConfig: backendConfig, customerAPI: customer, identityAPI: identity, offeringsAPI: offerings, offlineEntitlements: offlineEntitlements, - internalAPI: internalAPI) + internalAPI: internalAPI, + customerCenterConfig: customerCenterConfig) } override func post(receipt: EncodedAppleReceipt, diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift new file mode 100644 index 0000000000..4d4594133e --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -0,0 +1,377 @@ +// +// 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 +// +// BackendGetCustomerCenterConfigTests.swift +// +// Created by Cesar de la Vega on 29/6/24. + +import Foundation +import Nimble +import XCTest + +@testable import RevenueCat + +class BackendGetCustomerCenterConfigTests: BaseBackendTests { + + override func createClient() -> MockHTTPClient { + super.createClient(#file) + } + + func testGetCustomerCenterConfigCallsHTTPMethod() { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, response: Self.customerCenterResponse as [String: Any]) + ) + + let result = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: false, + completion: completed) + } + + expect(result).to(beSuccess()) + expect(self.httpClient.calls).to(haveCount(1)) + expect(self.operationDispatcher.invokedDispatchOnWorkerThreadDelayParam) == Delay.none + } + + func testGetCustomerCenterConfigPassesLocales() { + self.createDependencies(localesProvider: MockPreferredLocalesProvider(stubbedLocales: ["en_EN", "es_ES"])) + + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, response: Self.customerCenterResponse as [String: Any]) + ) + + let result = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: false, + completion: completed) + } + + expect(result).to(beSuccess()) + expect(self.httpClient.calls).to(haveCount(1)) + expect(self.httpClient.calls[0].headers["X-Preferred-Locales"]) == "en_EN,es_ES" + } + + func testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay() { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, response: Self.customerCenterResponse as [String: Any]) + ) + + let result = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: true, + completion: completed) + } + + expect(result).to(beSuccess()) + expect(self.httpClient.calls).to(haveCount(1)) + expect(self.operationDispatcher.invokedDispatchOnWorkerThreadDelayParam) == .default + } + + func testGetCustomerCenterConfigCachesForSameUserID() { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, + response: Self.customerCenterResponse as [String: Any], + delay: .milliseconds(10)) + ) + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, isAppBackgrounded: false) { _ in } + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, isAppBackgrounded: false) { _ in } + + expect(self.httpClient.calls).toEventually(haveCount(1)) + } + + func testRepeatedRequestsLogDebugMessage() { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, + response: Self.customerCenterResponse as [String: Any], + delay: .milliseconds(10)) + ) + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, isAppBackgrounded: false) { _ in } + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, isAppBackgrounded: false) { _ in } + + expect(self.httpClient.calls).toEventually(haveCount(1)) + + self.logger.verifyMessageWasLogged( + "Network operation '\(GetCustomerCenterConfigOperation.self)' found with the same cache key", + level: .debug + ) + } + + func testGetCustomerConfigDoesntCacheForMultipleUserID() { + let response = MockHTTPClient.Response(statusCode: .success, + response: Self.customerCenterResponse as [String: Any]) + let userID2 = "user_id_2" + + self.httpClient.mock(requestPath: .getCustomerCenterConfig(appUserID: Self.userID), response: response) + self.httpClient.mock(requestPath: .getCustomerCenterConfig(appUserID: userID2), response: response) + + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: false, + completion: { _ in }) + self.customerCenterConfig.getCustomerCenterConfig(appUserID: userID2, + isAppBackgrounded: false, + completion: { _ in }) + + expect(self.httpClient.calls).toEventually(haveCount(2)) + } + + func testGetCustomerCenterConfig() throws { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(statusCode: .success, response: Self.customerCenterResponse as [String: Any]) + ) + + let result: Atomic?> = nil + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, isAppBackgrounded: false) { + result.value = $0 + } + + expect(result.value).toEventuallyNot(beNil()) + + let response = try XCTUnwrap(result.value?.value) + let customerCenter = try XCTUnwrap(response.customerCenter) + let appearance = try XCTUnwrap(customerCenter.appearance) + + expect(customerCenter.localization.locale) == "en_US" + expect(customerCenter.localization.localizedStrings).to(haveCount(2)) + + expect(appearance.dark.accentColor) == "#ffffff" + expect(appearance.dark.backgroundColor) == "#000000" + expect(appearance.dark.textColor) == "#000000" + expect(appearance.light.accentColor) == "#000000" + expect(appearance.light.backgroundColor) == "#ffffff" + expect(appearance.light.textColor) == "#ffffff" + expect(appearance.mode) == "CUSTOM" + + let screens = try XCTUnwrap(customerCenter.screens) + expect(screens).to(haveCount(2)) + + let noActiveScreen = try XCTUnwrap(customerCenter.screens[ + CustomerCenterConfigData.Screen.ScreenType.noActive.rawValue + ]) + expect(noActiveScreen.title) == "No subscriptions found" + expect(noActiveScreen.subtitle) == "We can try checking your account for any previous purchases" + + let managementScreen = try XCTUnwrap(customerCenter.screens[ + CustomerCenterConfigData.Screen.ScreenType.management.rawValue + ]) + expect(managementScreen.type) == .management + expect(managementScreen.title) == "How can we help?" + + let noActiveScreenPaths = noActiveScreen.paths + expect(noActiveScreenPaths).to(haveCount(1)) + + let managementPaths = managementScreen.paths + expect(managementPaths).to(haveCount(4)) + + let path1 = managementPaths[0] + expect(path1.id) == "ownmsldfow" + expect(path1.title) == "Didn't receive purchase" + expect(path1.type) == .missingPurchase + + let path2 = managementPaths[1] + expect(path2.id) == "nwodkdnfaoeb" + expect(path2.title) == "Request a refund" + expect(path2.type) == .refundRequest + let promotionalOffer1 = try XCTUnwrap(path2.promotionalOffer) + expect(promotionalOffer1.iosOfferId) == "rc-refund-offer" + + let path3 = managementPaths[2] + expect(path3.id) == "nfoaiodifj9" + expect(path3.title) == "Change plans" + expect(path3.type) == .changePlans + + let path4 = managementPaths[3] + expect(path4.id) == "jnkasldfhas" + expect(path4.title) == "Cancel subscription" + expect(path4.type) == .cancel + + let feedbackSurvey = try XCTUnwrap(path4.feedbackSurvey) + expect(feedbackSurvey.title) == "Why are you cancelling?" + expect(feedbackSurvey.options).to(haveCount(3)) + + let option1 = feedbackSurvey.options[0] + expect(option1.id) == "iewrthals" + expect(option1.title) == "Too expensive" + let promotionalOffer2 = try XCTUnwrap(option1.promotionalOffer) + expect(promotionalOffer2.iosOfferId) == "rc-cancel-offer" + + let option2 = feedbackSurvey.options[1] + expect(option2.id) == "qklpadsfj" + expect(option2.title) == "Don't use the app" + let promotionalOffer3 = try XCTUnwrap(option2.promotionalOffer) + expect(promotionalOffer3.iosOfferId) == "rc-cancel-offer" + + let option3 = feedbackSurvey.options[2] + expect(option3.id) == "jargnapocps" + expect(option3.title) == "Bought by mistake" + } + + func testGetCustomerCenterConfigFailSendsNil() { + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(error: .unexpectedResponse(nil)) + ) + + let result = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: false, + completion: completed) + } + + expect(result).to(beFailure()) + } + + func testGetCustomerCenterConfigNetworkErrorSendsError() { + let mockedError: NetworkError = .unexpectedResponse(nil) + + self.httpClient.mock( + requestPath: .getCustomerCenterConfig(appUserID: Self.userID), + response: .init(error: mockedError) + ) + + let result = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: Self.userID, + isAppBackgrounded: false, + completion: completed) + } + + expect(result).to(beFailure()) + expect(result?.error) == .networkError(mockedError) + } + + func testGetCustomerCenterConfigSkipsBackendCallIfAppUserIDIsEmpty() { + waitUntil { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: "", isAppBackgrounded: false) { _ in + completed() + } + } + + expect(self.httpClient.calls).to(beEmpty()) + } + + func testGetCustomerCenterConfigCallsCompletionWithErrorIfAppUserIDIsEmpty() { + let receivedError = waitUntilValue { completed in + self.customerCenterConfig.getCustomerCenterConfig(appUserID: "", isAppBackgrounded: false) { result in + completed(result.error) + } + } + + expect(receivedError) == .missingAppUserID() + } + +} + +private extension BackendGetCustomerCenterConfigTests { + + static let customerCenterResponse: [String: Any] = [ + "customer_center": [ + "localization": [ + "locale": "en_US", + "localized_strings": [ + "cancel": "Cancel", + "back": "Back" + ] as [String: Any], + "supported": [ + "en_US" + ] as [Any] + ] as [String: Any], + "screens": [ + "MANAGEMENT": [ + "paths": [ + [ + "id": "ownmsldfow", + "title": "Didn't receive purchase", + "type": "MISSING_PURCHASE" + ] as [String: Any], + [ + "id": "nwodkdnfaoeb", + "promotional_offer": [ + "ios_offer_id": "rc-refund-offer", + "eligible": true + ] as [String: Any], + "title": "Request a refund", + "type": "REFUND_REQUEST" + ] as [String: Any], + [ + "id": "nfoaiodifj9", + "title": "Change plans", + "type": "CHANGE_PLANS" + ] as [String: Any], + [ + "feedback_survey": [ + "options": [ + [ + "id": "iewrthals", + "promotional_offer": [ + "ios_offer_id": "rc-cancel-offer", + "eligible": false + ] as [String: Any], + "title": "Too expensive" + ] as [String: Any], + [ + "id": "qklpadsfj", + "promotional_offer": [ + "ios_offer_id": "rc-cancel-offer", + "eligible": false + ] as [String: Any], + "title": "Don't use the app" + ] as [String: Any], + [ + "id": "jargnapocps", + "title": "Bought by mistake" + ] as [String: Any] + ] as [Any], + "title": "Why are you cancelling?" + ] as [String: Any], + "id": "jnkasldfhas", + "title": "Cancel subscription", + "type": "CANCEL" + ] as [String: Any] + ] as [Any], + "title": "How can we help?", + "type": "MANAGEMENT" + ] as [String: Any], + "NO_ACTIVE": [ + "paths": [ + [ + "id": "9q9719171o", + "title": "Check purchases", + "type": "MISSING_PURCHASE" + ] as [String: Any] + ] as [Any], + "subtitle": "We can try checking your account for any previous purchases", + "title": "No subscriptions found", + "type": "NO_ACTIVE" + ] as [String: Any] + ] as [String: Any], + "appearance": [ + "dark": [ + "accent_color": "#ffffff", + "background_color": "#000000", + "text_color": "#000000" + ] as [String: Any], + "light": [ + "accent_color": "#000000", + "background_color": "#ffffff", + "text_color": "#ffffff" + ] as [String: Any], + "mode": "CUSTOM" + ] as [String: Any], + "support": [ + "email": "support@revenuecat.com" + ] as [String: Any] + ] as [String: Any] + ] + +} diff --git a/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift b/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift index 79715570e4..be6b606075 100644 --- a/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift +++ b/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift @@ -32,6 +32,7 @@ class BaseBackendTests: TestCase { private(set) var offlineEntitlements: OfflineEntitlementsAPI! private(set) var identity: IdentityAPI! private(set) var internalAPI: InternalAPI! + private(set) var customerCenterConfig: CustomerCenterConfigAPI! static let apiKey = "asharedsecret" static let userID = "user" @@ -83,13 +84,15 @@ class BaseBackendTests: TestCase { self.offerings = OfferingsAPI(backendConfig: backendConfig) self.offlineEntitlements = OfflineEntitlementsAPI(backendConfig: backendConfig) self.internalAPI = InternalAPI(backendConfig: backendConfig) + self.customerCenterConfig = CustomerCenterConfigAPI(backendConfig: backendConfig) self.backend = Backend(backendConfig: backendConfig, customerAPI: customer, identityAPI: self.identity, offeringsAPI: self.offerings, offlineEntitlements: self.offlineEntitlements, - internalAPI: self.internalAPI) + internalAPI: self.internalAPI, + customerCenterConfig: self.customerCenterConfig) } var verificationMode: Configuration.EntitlementVerificationMode { diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfig.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..13b485ab69 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..58847be783 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS14-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..a972b5b8a7 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..40358468e5 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..70ecfa1cb5 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..a902ee3635 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..ed71050ec3 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,24 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerInfoTests/iOS17-testEncodesCustomerUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerInfoTests/iOS17-testEncodesCustomerUserID.1.json deleted file mode 100644 index d5c0e55a1b..0000000000 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerInfoTests/iOS17-testEncodesCustomerUserID.1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "headers" : { - "Authorization" : "Bearer asharedsecret" - }, - "request" : { - "body" : null, - "method" : "GET", - "url" : "https://api.revenuecat.com/v1/subscribers/userid%20with%20spaces" - } -} \ No newline at end of file From fec94860c96c88de67f3b139f4a49ffc03ec8933 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 11 Jul 2024 08:10:37 +0200 Subject: [PATCH 04/90] [Customer Center] Build feedback survey from JSON (#3959) Based off #3933 https://github.com/RevenueCat/purchases-ios/assets/664544/11eae984-294a-4e14-8c40-7c2d50994c09 Can probably use some animations, but tuning that up will come up later It will open a feedback survey for an option if there's one --- RevenueCat.xcodeproj/project.pbxproj | 8 ++ .../Data/FeedbackSurveyData.swift | 37 +++++++++ .../ManageSubscriptionsButtonStyle.swift | 4 +- .../ManageSubscriptionsViewModel.swift | 17 +++- .../Views/FeedbackSurveyView.swift | 82 +++++++++++++++++++ .../Views/ManageSubscriptionsView.swift | 37 ++++++++- 6 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 665727fe43..952d6a9c29 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -218,6 +218,8 @@ 35B745A82711001A00458D46 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; }; 35C05DC02BC84F5800109308 /* DiagnosticsSynchronizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C05DBF2BC84F5800109308 /* DiagnosticsSynchronizerTests.swift */; }; 35C05DC82BC8510000109308 /* DiagnosticsTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C05DC72BC8510000109308 /* DiagnosticsTrackerTests.swift */; }; + 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */; }; + 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */; }; 35C272A12BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; 35C272A22BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */; }; @@ -1194,6 +1196,8 @@ 35AB6D392BBEE3150076B103 /* DiagnosticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTracker.swift; sourceTree = ""; }; 35C05DBF2BC84F5800109308 /* DiagnosticsSynchronizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSynchronizerTests.swift; sourceTree = ""; }; 35C05DC72BC8510000109308 /* DiagnosticsTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTrackerTests.swift; sourceTree = ""; }; + 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyData.swift; sourceTree = ""; }; + 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyView.swift; sourceTree = ""; }; 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsTracker.swift; sourceTree = ""; }; 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorUtils.swift; sourceTree = ""; }; 35D159CA2BC4396F004D8061 /* DiagnosticsPostOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsPostOperation.swift; sourceTree = ""; }; @@ -2549,6 +2553,7 @@ children = ( 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, + 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */, 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */, ); path = Data; @@ -2568,6 +2573,7 @@ isa = PBXGroup; children = ( 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */, + 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */, 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */, 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */, 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, @@ -5209,6 +5215,7 @@ 887A60C12C1D037000E1A461 /* DebugErrorView.swift in Sources */, 887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */, 887A60782C1D037000E1A461 /* TestData.swift in Sources */, + 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */, 887A60672C1D037000E1A461 /* PaywallError.swift in Sources */, 88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */, 887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */, @@ -5217,6 +5224,7 @@ 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, + 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */, 887A60BA2C1D037000E1A461 /* Template3View.swift in Sources */, 887A607D2C1D037000E1A461 /* ImageLoader.swift in Sources */, 887A60822C1D037000E1A461 /* PreviewHelpers.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift new file mode 100644 index 0000000000..8797525399 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// FeedbackSurveyData.swift +// +// +// Created by Cesar de la Vega on 14/6/24. +// + +import Foundation +import RevenueCat + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +class FeedbackSurveyData: ObservableObject { + + var configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey + var action: (() -> Void) + + init(configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey, action: @escaping (() -> Void)) { + self.configuration = configuration + self.action = action + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index c4970d4ff7..0e29eb90a7 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -7,7 +7,7 @@ // // https://opensource.org/licenses/MIT // -// CustomButtonStyle.swift +// ManageSubscriptionsButtonStyle.swift // // // Created by Cesar de la Vega on 28/5/24. @@ -40,7 +40,7 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct CustomButtonStylePreview_Previews: PreviewProvider { +struct ManageSubscriptionsButtonStyle_Previews: PreviewProvider { static var previews: some View { Button("Didn't receive purchase") {} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index fa569ecadd..0d5835f78a 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -29,6 +29,9 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var showRestoreAlert: Bool = false + @Published + var feedbackSurveyData: FeedbackSurveyData? + @Published var state: CustomerCenterViewState { didSet { @@ -105,7 +108,19 @@ class ManageSubscriptionsViewModel: ObservableObject { } #if os(iOS) || targetEnvironment(macCatalyst) - func handleAction(for path: CustomerCenterConfigData.HelpPath) async { + func determineFlow(for path: CustomerCenterConfigData.HelpPath) async { + if case let .feedbackSurvey(feedbackSurvey) = path.detail { + self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in + Task { + await self?.performAction(for: path) + } + } + } else { + await self.performAction(for: path) + } + } + + func performAction(for path: CustomerCenterConfigData.HelpPath) async { switch path.type { case .missingPurchase: self.showRestoreAlert = true diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift new file mode 100644 index 0000000000..921cc22f04 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -0,0 +1,82 @@ +// +// 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 +// +// FeedbackSurveyView.swift +// +// +// Created by Cesar de la Vega on 12/6/24. +// + +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct FeedbackSurveyView: View { + + @ObservedObject + var feedbackSurveyData: FeedbackSurveyData + + var body: some View { + VStack { + Text(feedbackSurveyData.configuration.title) + .font(.title) + .padding() + + Spacer() + + FeedbackSurveyButtonsView(options: feedbackSurveyData.configuration.options, + action: feedbackSurveyData.action) + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct FeedbackSurveyButtonsView: View { + + let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] + let action: (() -> Void) + + var body: some View { + VStack(spacing: Self.buttonSpacing) { + ForEach(options, id: \.id) { option in + AsyncButton(action: { + self.action() + }, label: { + Text(option.title) + }) + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension FeedbackSurveyButtonsView { + + private static let buttonSpacing: CGFloat = 16 + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 4aee4f7911..fbf2912169 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -41,6 +41,38 @@ struct ManageSubscriptionsView: View { } var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + content + .navigationDestination(isPresented: .constant(self.viewModel.feedbackSurveyData != nil)) { + if let feedbackSurveyData = self.viewModel.feedbackSurveyData { + FeedbackSurveyView(feedbackSurveyData: feedbackSurveyData) + .onDisappear { + self.viewModel.feedbackSurveyData = nil + } + } + } + } + } else { + NavigationView { + content + .background(NavigationLink( + destination: self.viewModel.feedbackSurveyData.map { data in + FeedbackSurveyView(feedbackSurveyData: data) + .onDisappear { + self.viewModel.feedbackSurveyData = nil + } + }, + isActive: .constant(self.viewModel.feedbackSurveyData != nil) + ) { + EmptyView() + }) + } + } + } + + @ViewBuilder + var content: some View { VStack { if self.viewModel.isLoaded { HeaderView(viewModel: self.viewModel) @@ -53,6 +85,7 @@ struct ManageSubscriptionsView: View { Spacer() ManageSubscriptionsButtonsView(viewModel: self.viewModel) + } else { ProgressView() .progressViewStyle(CircularProgressViewStyle()) @@ -61,8 +94,8 @@ struct ManageSubscriptionsView: View { .task { await loadInformationIfNeeded() } + .navigationBarTitleDisplayMode(.inline) } - } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -158,7 +191,7 @@ struct ManageSubscriptionsButtonsView: View { } ForEach(filteredPaths, id: \.id) { path in AsyncButton(action: { - await self.viewModel.handleAction(for: path) + await self.viewModel.determineFlow(for: path) }, label: { Text(path.title) }) From b087782e14d320fe2990a37e0565601ff48645fc Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:16:03 +0200 Subject: [PATCH 05/90] [Customer Center] Show the right subscription on the Manage Subscription screen (#4046) --- .../ManageSubscriptionsViewModel.swift | 21 +- .../ManageSubscriptionsViewModelTests.swift | 510 +++++++++++++----- 2 files changed, 401 insertions(+), 130 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 0d5835f78a..cbacdcdae8 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -84,13 +84,24 @@ class ManageSubscriptionsViewModel: ObservableObject { private func loadSubscriptionInformation() async throws { let customerInfo = try await purchasesProvider.customerInfo() - guard let currentEntitlementDict = customerInfo.entitlements.active.first, - let subscribedProductID = customerInfo.activeSubscriptions.first, - let subscribedProduct = await purchasesProvider.products([subscribedProductID]).first else { + + // Pick the soonest expiring iOS App Store entitlement and accompanying product. + guard let currentEntitlement = customerInfo.entitlements + .active + .values + .lazy + .filter({ entitlement in entitlement.store == .appStore }) + .sorted(by: { lhs, rhs in + let lhsDateSeconds = lhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude + let rhsDateSeconds = rhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude + + return lhsDateSeconds < rhsDateSeconds + }).first, + let subscribedProduct = await purchasesProvider.products([currentEntitlement.productIdentifier]).first + else { Logger.warning(Strings.could_not_find_subscription_information) throw CustomerCenterError.couldNotFindSubscriptionInformation } - let currentEntitlement = currentEntitlementDict.value // swiftlint:disable:next todo // TODO: support non-consumables @@ -102,7 +113,7 @@ class ManageSubscriptionsViewModel: ObservableObject { price: subscribedProduct.localizedPriceString, nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, willRenew: currentEntitlement.willRenew, - productIdentifier: subscribedProductID, + productIdentifier: currentEntitlement.productIdentifier, active: currentEntitlement.isActive ) } diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index b3e29bb134..9f8978869e 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 11/6/24. // +// swiftlint:disable file_length type_body_length function_body_length + import Nimble import RevenueCat @testable import RevenueCatUI @@ -71,27 +73,240 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(viewModel.isLoaded) == true } - func testLoadScreenSuccess() async { + func testShouldShowActiveSubscription_whenUserHasOneActiveSubscriptionOneEntitlement() async throws { + // Arrange + let productId = "com.revenuecat.product" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDate = "2062-04-12T00:03:35Z" + let products = [Fixtures.product(id: productId, title: "title", duration: .month, price: 2.99)] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productId, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ], + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productId, + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ] + ) + + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + )) + + // Act + await viewModel.loadScreen() + + // Assert + expect(viewModel.screen).toNot(beNil()) + expect(viewModel.state) == .success + + let subscriptionInformation = try XCTUnwrap(viewModel.subscriptionInformation) + expect(subscriptionInformation.title) == "title" + expect(subscriptionInformation.durationTitle) == "month" + expect(subscriptionInformation.price) == "$2.99" + expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDate) + expect(subscriptionInformation.productIdentifier) == productId + } + + func testShouldShowEarliestExpiration_whenUserHasTwoActiveSubscriptionsOneEntitlement() async throws { + // Arrange + let productIdOne = "com.revenuecat.product1" + let productIdTwo = "com.revenuecat.product2" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDateFirst = "2062-04-12T00:03:35Z" + let expirationDateSecond = "2062-05-12T00:03:35Z" + let products = [ + Fixtures.product(id: productIdOne, title: "yearly", duration: .year, price: 29.99), + Fixtures.product(id: productIdTwo, title: "monthly", duration: .month, price: 2.99) + ] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productIdOne, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Subscription( + id: productIdTwo, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled(), + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productIdOne, + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ) + ] + ) + + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + )) + + // Act + await viewModel.loadScreen() + + // Assert + expect(viewModel.screen).toNot(beNil()) + expect(viewModel.state) == .success + + let subscriptionInformation = try XCTUnwrap(viewModel.subscriptionInformation) + expect(subscriptionInformation.title) == "yearly" + expect(subscriptionInformation.durationTitle) == "year" + expect(subscriptionInformation.price) == "$29.99" + expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateFirst) + expect(subscriptionInformation.productIdentifier) == productIdOne + } + + func testShouldShowEarliestExpiration_whenUserHasTwoActiveSubscriptionsTwoEntitlements() async throws { + // Arrange + let productIdOne = "com.revenuecat.product1" + let productIdTwo = "com.revenuecat.product2" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDateFirst = "2062-04-12T00:03:35Z" + let expirationDateSecond = "2062-05-12T00:03:35Z" + let products = [ + Fixtures.product(id: productIdOne, title: "yearly", duration: .year, price: 29.99), + Fixtures.product(id: productIdTwo, title: "monthly", duration: .month, price: 2.99) + ] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productIdOne, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Subscription( + id: productIdTwo, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled(), + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productIdOne, + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Entitlement( + entitlementId: "plus", + productId: productIdTwo, + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled() + ) + + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + )) + + // Act + await viewModel.loadScreen() + + // Assert + expect(viewModel.screen).toNot(beNil()) + expect(viewModel.state) == .success + + let subscriptionInformation = try XCTUnwrap(viewModel.subscriptionInformation) + expect(subscriptionInformation.title) == "yearly" + expect(subscriptionInformation.durationTitle) == "year" + expect(subscriptionInformation.price) == "$29.99" + expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateFirst) + expect(subscriptionInformation.productIdentifier) == productIdOne + } + + func testShouldShowAppleSubscription_whenUserHasBothGoogleAndAppleSubscriptions() async throws { + // Arrange + let productIdOne = "com.revenuecat.product1" + let productIdTwo = "com.revenuecat.product2" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDateFirst = "2062-04-12T00:03:35Z" + let expirationDateSecond = "2062-05-12T00:03:35Z" + let products = [ + Fixtures.product(id: productIdOne, title: "yearly", duration: .year, price: 29.99), + Fixtures.product(id: productIdTwo, title: "monthly", duration: .month, price: 2.99) + ] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productIdOne, + store: "play_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Subscription( + id: productIdTwo, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled(), + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productIdOne, + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Entitlement( + entitlementId: "plus", + productId: productIdTwo, + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled() + ) + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, - purchasesProvider: MockManageSubscriptionsPurchases()) + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + )) + // Act await viewModel.loadScreen() - expect(viewModel.subscriptionInformation).toNot(beNil()) + // Assert expect(viewModel.screen).toNot(beNil()) expect(viewModel.state) == .success - expect(viewModel.subscriptionInformation?.title) == "title" - expect(viewModel.subscriptionInformation?.durationTitle) == "month" - expect(viewModel.subscriptionInformation?.price) == "$2.99" - expect(viewModel.subscriptionInformation?.nextRenewalString) == "Apr 12, 2062" - expect(viewModel.subscriptionInformation?.productIdentifier) == "com.revenuecat.product" + let subscriptionInformation = try XCTUnwrap(viewModel.subscriptionInformation) + // We expect to see the monthly one, because the yearly one is a Google subscription. + expect(subscriptionInformation.title) == "monthly" + expect(subscriptionInformation.durationTitle) == "month" + expect(subscriptionInformation.price) == "$2.99" + expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateSecond) + expect(subscriptionInformation.productIdentifier) == productIdTwo } func testLoadScreenNoActiveSubscription() async { let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, purchasesProvider: MockManageSubscriptionsPurchases( - customerInfo: ManageSubscriptionsViewModelTests.customerInfoWithoutSubscriptions + customerInfo: Fixtures.customerInfoWithoutSubscriptions )) await viewModel.loadScreen() @@ -112,27 +327,37 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(viewModel.state) == .error(error) } + private func reformat(ISO8601Date: String) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: ISO8601DateFormatter().date(from: ISO8601Date)!) + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { - let customerInfo: CustomerInfo? + let customerInfo: CustomerInfo let customerInfoError: Error? - let productsShouldFail: Bool + // StoreProducts keyed by productIdentifier. + let products: [String: RevenueCat.StoreProduct] let showManageSubscriptionsError: Error? let beginRefundShouldFail: Bool init( - customerInfo: CustomerInfo? = nil, + customerInfo: CustomerInfo = Fixtures.customerInfoWithAppleSubscriptions, customerInfoError: Error? = nil, - productsShouldFail: Bool = false, + products: [RevenueCat.StoreProduct] = + [Fixtures.product(id: "com.revenuecat.product", title: "title", duration: .month, price: 2.99)], showManageSubscriptionsError: Error? = nil, beginRefundShouldFail: Bool = false ) { self.customerInfo = customerInfo self.customerInfoError = customerInfoError - self.productsShouldFail = productsShouldFail + self.products = Dictionary(uniqueKeysWithValues: products.map({ product in + (product.productIdentifier, product) + })) self.showManageSubscriptionsError = showManageSubscriptionsError self.beginRefundShouldFail = beginRefundShouldFail } @@ -141,18 +366,13 @@ final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { if let customerInfoError { throw customerInfoError } - if let customerInfo { - return customerInfo - } - return await ManageSubscriptionsViewModelTests.customerInfoWithAppleSubscriptions + return customerInfo } func products(_ productIdentifiers: [String]) async -> [RevenueCat.StoreProduct] { - if productsShouldFail { - return [] + return productIdentifiers.compactMap { productIdentifier in + products[productIdentifier] } - let product = await ManageSubscriptionsViewModelTests.createMockProduct() - return [product] } func showManageSubscriptions() async throws { @@ -171,19 +391,81 @@ final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -private extension ManageSubscriptionsViewModelTests { +private class Fixtures { + + private init() {} + + class Subscription { + + let id: String + let json: String + + init(id: String, store: String, purchaseDate: String, expirationDate: String) { + self.id = id + self.json = """ + { + "billing_issues_detected_at": null, + "expires_date": "\(expirationDate)", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "\(purchaseDate)", + "period_type": "intro", + "purchase_date": "\(purchaseDate)", + "store": "\(store)", + "unsubscribe_detected_at": null + } + """ + } - static let screen: CustomerCenterConfigData.Screen = - CustomerCenterConfigTestData.customerCenterData.screens[.management]! + } - static func createMockProduct() -> StoreProduct { + class Entitlement { + + let id: String + let json: String + + init(entitlementId: String, productId: String, purchaseDate: String, expirationDate: String) { + self.id = entitlementId + self.json = """ + { + "expires_date": "\(expirationDate)", + "product_identifier": "\(productId)", + "purchase_date": "\(purchaseDate)" + } + """ + } + + } + + static func product( + id: String, + title: String, + duration: SKProduct.PeriodUnit, + price: Decimal, + priceLocale: String = "en_US" + ) -> StoreProduct { // Using SK1 products because they can be mocked, but CustomerCenterViewModel // works with generic `StoreProduct`s regardless of what they contain - return StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "identifier", - mockLocalizedTitle: "title")) + let sk1Product = MockSK1Product(mockProductIdentifier: id, mockLocalizedTitle: title) + sk1Product.mockPrice = price + sk1Product.mockPriceLocale = Locale(identifier: priceLocale) + sk1Product.mockSubscriptionPeriod = SKProductSubscriptionPeriod(numberOfUnits: 1, unit: duration) + return StoreProduct(sk1Product: sk1Product) } - static let customerInfoWithAppleSubscriptions: CustomerInfo = { + static func customerInfo(subscriptions: [Subscription], entitlements: [Entitlement]) -> CustomerInfo { + let subscriptionsJson = subscriptions.map { subscription in + """ + "\(subscription.id)": \(subscription.json) + """ + }.joined(separator: ",\n") + + let entitlementsJson = entitlements.map { entitlement in + """ + "\(entitlement.id)": \(entitlement.json) + """ + }.joined(separator: ",\n") + return .decode( """ { @@ -202,121 +484,99 @@ private extension ManageSubscriptionsViewModelTests { "other_purchases": { }, "subscriptions": { - "com.revenuecat.product": { - "billing_issues_detected_at": null, - "expires_date": "2062-04-12T00:03:35Z", - "grace_period_expires_date": null, - "is_sandbox": true, - "original_purchase_date": "2022-04-12T00:03:28Z", - "period_type": "intro", - "purchase_date": "2022-04-12T00:03:28Z", - "store": "app_store", - "unsubscribe_detected_at": null - }, + \(subscriptionsJson) }, "entitlements": { - "premium": { - "expires_date": "2062-04-12T00:03:35Z", - "product_identifier": "com.revenuecat.product", - "purchase_date": "2022-04-12T00:03:28Z" - } + \(entitlementsJson) } } } """ ) + } + + static let customerInfoWithAppleSubscriptions: CustomerInfo = { + let productId = "com.revenuecat.product" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDate = "2062-04-12T00:03:35Z" + return customerInfo( + subscriptions: [ + Subscription( + id: productId, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ], + entitlements: [ + Entitlement( + entitlementId: "premium", + productId: productId, + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ] + ) }() static let customerInfoWithGoogleSubscriptions: CustomerInfo = { - return .decode( - """ - { - "schema_version": "4", - "request_date": "2022-03-08T17:42:58Z", - "request_date_ms": 1646761378845, - "subscriber": { - "first_seen": "2022-03-08T17:42:58Z", - "last_seen": "2022-03-08T17:42:58Z", - "management_url": "https://apps.apple.com/account/subscriptions", - "non_subscriptions": { - }, - "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", - "original_application_version": "1.0", - "original_purchase_date": "2022-04-12T00:03:24Z", - "other_purchases": { - }, - "subscriptions": { - "com.revenuecat.product": { - "billing_issues_detected_at": null, - "expires_date": "2062-04-12T00:03:35Z", - "grace_period_expires_date": null, - "is_sandbox": true, - "original_purchase_date": "2022-04-12T00:03:28Z", - "period_type": "intro", - "purchase_date": "2022-04-12T00:03:28Z", - "store": "play_store", - "unsubscribe_detected_at": null - }, - }, - "entitlements": { - "premium": { - "expires_date": "2062-04-12T00:03:35Z", - "product_identifier": "com.revenuecat.product", - "purchase_date": "2022-04-12T00:03:28Z" - } - } - } - } - """ + let productId = "com.revenuecat.product" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDate = "2062-04-12T00:03:35Z" + return customerInfo( + subscriptions: [ + Subscription( + id: productId, + store: "play_store", + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ], + entitlements: [ + Entitlement( + entitlementId: "premium", + productId: productId, + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ] ) }() static let customerInfoWithoutSubscriptions: CustomerInfo = { - return .decode( - """ - { - "schema_version": "4", - "request_date": "2022-03-08T17:42:58Z", - "request_date_ms": 1646761378845, - "subscriber": { - "first_seen": "2022-03-08T17:42:58Z", - "last_seen": "2022-03-08T17:42:58Z", - "management_url": "https://apps.apple.com/account/subscriptions", - "non_subscriptions": { - }, - "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", - "original_application_version": "1.0", - "original_purchase_date": "2022-04-12T00:03:24Z", - "other_purchases": { - }, - "subscriptions": { - "com.revenuecat.product": { - "billing_issues_detected_at": null, - "expires_date": "2000-04-12T00:03:35Z", - "grace_period_expires_date": null, - "is_sandbox": true, - "original_purchase_date": "1999-04-12T00:03:28Z", - "period_type": "intro", - "purchase_date": "1999-04-12T00:03:28Z", - "store": "play_store", - "unsubscribe_detected_at": null - }, - }, - "entitlements": { - "premium": { - "expires_date": "2000-04-12T00:03:35Z", - "product_identifier": "com.revenuecat.product", - "purchase_date": "1999-04-12T00:03:28Z" - } - } - } - } - """ + let productId = "com.revenuecat.product" + let purchaseDate = "1999-04-12T00:03:28Z" + let expirationDate = "2000-04-12T00:03:35Z" + return customerInfo( + subscriptions: [ + Subscription( + id: productId, + store: "play_store", + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ], + entitlements: [ + Entitlement( + entitlementId: "premium", + productId: productId, + purchaseDate: purchaseDate, + expirationDate: expirationDate + ) + ] ) }() } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension ManageSubscriptionsViewModelTests { + + static let screen: CustomerCenterConfigData.Screen = + CustomerCenterConfigTestData.customerCenterData.screens[.management]! + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private class MockSK1Product: SK1Product { var mockProductIdentifier: String From 6cd489fbfa94424b14a6fbaf4d5b797f5fe1ee45 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Tue, 16 Jul 2024 18:09:20 +0200 Subject: [PATCH 06/90] [CustomerCenter] Add `presentCustomerCenter` modifier (#4053) ### Description This adds a modifier, `presentCustomerCenter` that can be used to more simply display the customer center. The API looks like: ``` .presentCustomerCenter(isPresented: self.$presentingCustomerCenter) { self.presentingCustomerCenter = false } ``` --- RevenueCat.xcodeproj/project.pbxproj | 4 + .../View+PresentCustomerCenter.swift | 126 ++++++++++++++++++ .../UI/Views/SamplePaywallsList.swift | 16 +++ 3 files changed, 146 insertions(+) create mode 100644 RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 952d6a9c29..3fd53ba65d 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 1E473B662AC42D34008B07F9 /* StoreMessagesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E473B652AC42D34008B07F9 /* StoreMessagesHelper.swift */; }; 1E473B682AC43254008B07F9 /* StoreMessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E473B672AC43254008B07F9 /* StoreMessageType.swift */; }; 1E568B512ACC6A8300D3C12F /* StoreMessageTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E568B502ACC6A8300D3C12F /* StoreMessageTypeTests.swift */; }; + 1E5F8F6E2C4515430041EECD /* View+PresentCustomerCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */; }; 1E99F81F2AC5917F0023E26E /* StoreMessagesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */; }; 2C0B98CD2797070B00C5874F /* PromotionalOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */; }; 2C6CC1162B8D2B6900432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6CC1152B8D2B6800432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift */; }; @@ -1047,6 +1048,7 @@ 1E473B672AC43254008B07F9 /* StoreMessageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessageType.swift; sourceTree = ""; }; 1E473B692AC46908008B07F9 /* MockStoreMessagesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreMessagesHelper.swift; sourceTree = ""; }; 1E568B502ACC6A8300D3C12F /* StoreMessageTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessageTypeTests.swift; sourceTree = ""; }; + 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+PresentCustomerCenter.swift"; sourceTree = ""; }; 1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessagesHelperTests.swift; sourceTree = ""; }; 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOffer.swift; sourceTree = ""; }; 2C646C282A0EBD0300E5936E /* CI-Snapshots.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "CI-Snapshots.xctestplan"; path = "Tests/TestPlans/CI-Snapshots.xctestplan"; sourceTree = ""; }; @@ -2591,6 +2593,7 @@ 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */, 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */, 353756632C382C2800A1B8D6 /* URLUtilities.swift */, + 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */, ); path = CustomerCenter; sourceTree = ""; @@ -5212,6 +5215,7 @@ 887A60C82C1D037000E1A461 /* ProgressView.swift in Sources */, 887A60D02C1D037000E1A461 /* View+PurchaseRestoreCompleted.swift in Sources */, 887A60CD2C1D037000E1A461 /* PaywallView.swift in Sources */, + 1E5F8F6E2C4515430041EECD /* View+PresentCustomerCenter.swift in Sources */, 887A60C12C1D037000E1A461 /* DebugErrorView.swift in Sources */, 887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */, 887A60782C1D037000E1A461 /* TestData.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift new file mode 100644 index 0000000000..14015ec5ef --- /dev/null +++ b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift @@ -0,0 +1,126 @@ +// +// 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 +// +// View+PresentCustomerCenter.swift +// +// Created by Toni Rico Diez on 2024-07-15. + +import RevenueCat +import SwiftUI + +#if os(iOS) + +/// Presentation options to use with the [presentCustomerCenter](x-source-tag://presentCustomerCenter) View modifiers. +public enum CustomerCenterPresentationMode { + + /// Customer center presented using SwiftUI's `.sheet`. + case sheet + + /// Customer center presented using SwiftUI's `.fullScreenCover`. + case fullScreen + +} + +extension CustomerCenterPresentationMode { + + // swiftlint:disable:next missing_docs + public static let `default`: Self = .sheet + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") +@available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") +@available(watchOS, unavailable, message: "CustomerCenterView does not support watchOS yet") +@available(visionOS, unavailable, message: "CustomerCenterView does not support visionOS yet") +extension View { + + /// Presents the ``CustomerCenter``. + /// Example: + /// ```swift + /// var body: some View { + /// YourApp() + /// .presentCustomerCenter() + /// } + /// ``` + /// - Parameter isPresented: Binding indicating whether the customer center should be displayed + /// - Parameter onDismiss: Callback executed when the customer center wants to be dismissed. + /// Make sure you stop presenting the customer center when this is called + /// - Parameter presentationMode: The desired presentation mode of the customer center. Defaults to `.sheet`. + public func presentCustomerCenter( + isPresented: Binding, + onDismiss: @escaping () -> Void, + presentationMode: CustomerCenterPresentationMode = .default + ) -> some View { + return self.modifier(PresentingCustomerCenterModifier( + isPresented: isPresented, + onDismiss: onDismiss, + myAppPurchaseLogic: nil, + presentationMode: presentationMode + )) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +private struct PresentingCustomerCenterModifier: ViewModifier { + + var presentationMode: CustomerCenterPresentationMode + var onDismiss: (() -> Void) + + init( + isPresented: Binding, + onDismiss: @escaping () -> Void, + myAppPurchaseLogic: MyAppPurchaseLogic?, + presentationMode: CustomerCenterPresentationMode, + purchaseHandler: PurchaseHandler? = nil + ) { + self._isPresented = isPresented + self.presentationMode = presentationMode + self.onDismiss = onDismiss + self._purchaseHandler = .init(wrappedValue: purchaseHandler ?? + PurchaseHandler.default(performPurchase: myAppPurchaseLogic?.performPurchase, + performRestore: myAppPurchaseLogic?.performRestore)) + } + + @StateObject + private var purchaseHandler: PurchaseHandler + + @Binding + var isPresented: Bool + + func body(content: Content) -> some View { + Group { + switch presentationMode { + case .sheet: + content + .sheet(isPresented: self.$isPresented, onDismiss: self.onDismiss) { + self.customerCenterView() + } + case .fullScreen: + content + .fullScreenCover(isPresented: self.$isPresented, onDismiss: self.onDismiss) { + self.customerCenterView() + } + } + } + } + + private func customerCenterView() -> some View { + CustomerCenterView() + .interactiveDismissDisabled(self.purchaseHandler.actionInProgress) + } + +} + +#endif diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index df525b9829..1c6d8f368a 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -16,6 +16,9 @@ struct SamplePaywallsList: View { @State private var display: Display? + @State + private var presentingCustomerCenter: Bool = false + var body: some View { NavigationView { self.list(with: Self.loader) @@ -136,10 +139,23 @@ struct SamplePaywallsList: View { } label: { TemplateLabel(name: "Unrecognized paywall", icon: "exclamationmark.triangle") } + + #if os(iOS) + Button { + self.presentingCustomerCenter = true + } label: { + TemplateLabel(name: "Customer center (sheet)", icon: "person.fill") + } + #endif } } .frame(maxWidth: .infinity) .buttonStyle(.plain) + #if os(iOS) + .presentCustomerCenter(isPresented: self.$presentingCustomerCenter) { + self.presentingCustomerCenter = false + } + #endif } #if os(watchOS) From 5ea67b21aa050c1591f7fa17029456be5f83a43b Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:49:11 +0200 Subject: [PATCH 07/90] [Customer Center] Adds the Customer Center to PaywallsTester (#4055) --- .../UI/Views/SamplePaywallsList.swift | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index 1c6d8f368a..617b8e9087 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -87,6 +87,8 @@ struct SamplePaywallsList: View { introEligibility: Self.introEligibility ) ) + case .customerCenter: + CustomerCenterView() } } @@ -139,15 +141,23 @@ struct SamplePaywallsList: View { } label: { TemplateLabel(name: "Unrecognized paywall", icon: "exclamationmark.triangle") } - - #if os(iOS) + } + + #if os(iOS) + Section("Customer Center") { + Button { + self.display = .customerCenter + } label: { + TemplateLabel(name: "SwiftUI", icon: "person.fill.questionmark") + } + Button { self.presentingCustomerCenter = true } label: { - TemplateLabel(name: "Customer center (sheet)", icon: "person.fill") + TemplateLabel(name: "Sheet", icon: "person.fill") } - #endif } + #endif } .frame(maxWidth: .infinity) .buttonStyle(.plain) @@ -207,6 +217,7 @@ private extension SamplePaywallsList { case customPaywall(PaywallViewMode) case missingPaywall case unrecognizedPaywall + case customerCenter } @@ -230,6 +241,9 @@ extension SamplePaywallsList.Display: Identifiable { case .unrecognizedPaywall: return "unrecognized" + + case .customerCenter: + return "customer-center" } } From 5030a3cef018d936cba59c15a5e5f2aa3451c185 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 18 Jul 2024 09:18:17 +0200 Subject: [PATCH 08/90] [Customer Center] Improves the UI of the current subscription (#4072) --- .../Data/CustomerCenterConfigTestData.swift | 16 +++- .../Data/SubscriptionInformation.swift | 18 +++-- .../ManageSubscriptionsViewModel.swift | 2 +- .../Views/ManageSubscriptionsView.swift | 81 +++++++++++++++---- .../ManageSubscriptionsViewModelTests.swift | 8 +- 5 files changed, 95 insertions(+), 30 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 7162901f85..b846d75128 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -104,14 +104,24 @@ enum CustomerCenterConfigTestData { ) ) - static let subscriptionInformation: SubscriptionInformation = .init( + static let subscriptionInformationMonthlyRenewing: SubscriptionInformation = .init( title: "Basic", durationTitle: "Monthly", - price: "$4.99 / month", - nextRenewalString: "June 1st, 2024", + price: "$4.99", + expirationDateString: "June 1st, 2024", willRenew: true, productIdentifier: "product_id", active: true ) + static let subscriptionInformationYearlyExpiring: SubscriptionInformation = .init( + title: "Basic", + durationTitle: "Yearly", + price: "$49.99", + expirationDateString: "June 1st, 2024", + willRenew: false, + productIdentifier: "product_id", + active: true + ) + } diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 277120b163..8e6a62d984 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -20,11 +20,19 @@ struct SubscriptionInformation { let title: String let durationTitle: String let price: String - let nextRenewalString: String? + let expirationDateString: String? let productIdentifier: String - var renewalString: String { - return active ? (willRenew ? "Renews" : "Expires") : "Expired" + var expirationString: String { + return active ? (willRenew ? "Next billing date" : "Expires") : "Expired" + } + + var explanation: String { + return active ? ( + willRenew ? + "This is your subscription with the earliest billing date." : + "This is your subscription with the earliest expiration date." + ) : "This subscription has expired." } private let willRenew: Bool @@ -33,7 +41,7 @@ struct SubscriptionInformation { init(title: String, durationTitle: String, price: String, - nextRenewalString: String?, + expirationDateString: String?, willRenew: Bool, productIdentifier: String, active: Bool @@ -41,7 +49,7 @@ struct SubscriptionInformation { self.title = title self.durationTitle = durationTitle self.price = price - self.nextRenewalString = nextRenewalString + self.expirationDateString = expirationDateString self.productIdentifier = productIdentifier self.willRenew = willRenew self.active = active diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index cbacdcdae8..9cd17aea55 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -111,7 +111,7 @@ class ManageSubscriptionsViewModel: ObservableObject { title: subscribedProduct.localizedTitle, durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", price: subscribedProduct.localizedPriceString, - nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, + expirationDateString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, willRenew: currentEntitlement.willRenew, productIdentifier: currentEntitlement.productIdentifier, active: currentEntitlement.isActive diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index fbf2912169..aeb516c890 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -137,35 +137,74 @@ struct HeaderView: View { @available(watchOS, unavailable) struct SubscriptionDetailsView: View { + let iconWidth = 22.0 let subscriptionInformation: SubscriptionInformation let refundRequestStatusMessage: String? var body: some View { VStack(alignment: .leading, spacing: 8) { - Text("\(subscriptionInformation.title) - \(subscriptionInformation.durationTitle)") - .font(.subheadline) - .padding([.horizontal, .top]) + VStack(alignment: .leading) { + Text("\(subscriptionInformation.title)") + .font(.headline) + Text("\(subscriptionInformation.explanation)") + .frame(maxWidth: 200, alignment: .leading) + .font(.caption) + .foregroundColor(Color(UIColor.secondaryLabel)) + }.padding([.bottom], 10) + + HStack(alignment: .center) { + Image(systemName: "coloncurrencysign.arrow.circlepath") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text("Billing cycle") + .font(.caption2) + .foregroundColor(Color(UIColor.secondaryLabel)) + Text("\(subscriptionInformation.durationTitle)") + .font(.caption) + } + } - Text("\(subscriptionInformation.price)") - .font(.caption) - .foregroundColor(Color.gray) - .padding(.horizontal) + HStack(alignment: .center) { + Image(systemName: "coloncurrencysign") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text("Current price") + .font(.caption2) + .foregroundColor(Color(UIColor.secondaryLabel)) + Text("\(subscriptionInformation.price)") + .font(.caption) + } + } - if let nextRenewal = subscriptionInformation.nextRenewalString { - Text("\(subscriptionInformation.renewalString): \(String(describing: nextRenewal))") - .font(.caption) - .foregroundColor(Color.gray) - .padding([.horizontal, .bottom]) + if let nextRenewal = subscriptionInformation.expirationDateString { + HStack(alignment: .center) { + Image(systemName: "calendar") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text("\(subscriptionInformation.expirationString)") + .font(.caption2) + .foregroundColor(Color(UIColor.secondaryLabel)) + Text("\(String(describing: nextRenewal))") + .font(.caption) + } + } } if let refundRequestStatusMessage = refundRequestStatusMessage { Text("Refund request status: \(refundRequestStatusMessage)") .font(.caption) .bold() - .foregroundColor(Color.gray) + .foregroundColor(Color(UIColor.secondaryLabel)) .padding([.horizontal, .bottom]) } - } + }.padding() + .padding(.horizontal) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(20) + .shadow(color: Color.black.opacity(0.2), radius: 4) } } @@ -213,10 +252,18 @@ struct ManageSubscriptionsButtonsView: View { struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { - let viewModel = ManageSubscriptionsViewModel( + let viewModelMonthlyRenewing = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, - subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformation) - ManageSubscriptionsView(viewModel: viewModel) + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationMonthlyRenewing) + ManageSubscriptionsView(viewModel: viewModelMonthlyRenewing) + .previewDisplayName("Monthly renewing") + + let viewModelYearlyExpiring = ManageSubscriptionsViewModel( + screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationYearlyExpiring) + + ManageSubscriptionsView(viewModel: viewModelYearlyExpiring) + .previewDisplayName("Yearly expiring") } } diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 9f8978869e..04c8bc811e 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -115,7 +115,7 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(subscriptionInformation.title) == "title" expect(subscriptionInformation.durationTitle) == "month" expect(subscriptionInformation.price) == "$2.99" - expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDate) + expect(subscriptionInformation.expirationDateString) == reformat(ISO8601Date: expirationDate) expect(subscriptionInformation.productIdentifier) == productId } @@ -172,7 +172,7 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(subscriptionInformation.title) == "yearly" expect(subscriptionInformation.durationTitle) == "year" expect(subscriptionInformation.price) == "$29.99" - expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateFirst) + expect(subscriptionInformation.expirationDateString) == reformat(ISO8601Date: expirationDateFirst) expect(subscriptionInformation.productIdentifier) == productIdOne } @@ -235,7 +235,7 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(subscriptionInformation.title) == "yearly" expect(subscriptionInformation.durationTitle) == "year" expect(subscriptionInformation.price) == "$29.99" - expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateFirst) + expect(subscriptionInformation.expirationDateString) == reformat(ISO8601Date: expirationDateFirst) expect(subscriptionInformation.productIdentifier) == productIdOne } @@ -299,7 +299,7 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(subscriptionInformation.title) == "monthly" expect(subscriptionInformation.durationTitle) == "month" expect(subscriptionInformation.price) == "$2.99" - expect(subscriptionInformation.nextRenewalString) == reformat(ISO8601Date: expirationDateSecond) + expect(subscriptionInformation.expirationDateString) == reformat(ISO8601Date: expirationDateSecond) expect(subscriptionInformation.productIdentifier) == productIdTwo } From 186804424bf4debe7d0d672d165f6464ee6b006b Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:40:45 +0200 Subject: [PATCH 09/90] [Customer Center] Updates the presentation of the refund status (#4082) --- .../ManageSubscriptionsViewModel.swift | 4 +++- .../Views/ManageSubscriptionsView.swift | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 9cd17aea55..9fe4e5c911 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -66,10 +66,12 @@ class ManageSubscriptionsViewModel: ObservableObject { } init(screen: CustomerCenterConfigData.Screen, - subscriptionInformation: SubscriptionInformation) { + subscriptionInformation: SubscriptionInformation, + refundRequestStatusMessage: String? = nil) { self.screen = screen self.subscriptionInformation = subscriptionInformation self.purchasesProvider = ManageSubscriptionPurchases() + self.refundRequestStatusMessage = refundRequestStatusMessage state = .success } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index aeb516c890..525b6e3d49 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -194,11 +194,18 @@ struct SubscriptionDetailsView: View { } if let refundRequestStatusMessage = refundRequestStatusMessage { - Text("Refund request status: \(refundRequestStatusMessage)") - .font(.caption) - .bold() - .foregroundColor(Color(UIColor.secondaryLabel)) - .padding([.horizontal, .bottom]) + HStack(alignment: .center) { + Image(systemName: "arrowshape.turn.up.backward") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text("Refund status") + .font(.caption2) + .foregroundColor(Color(UIColor.secondaryLabel)) + Text("\(refundRequestStatusMessage)") + .font(.caption) + } + } } }.padding() .padding(.horizontal) @@ -254,7 +261,8 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { let viewModelMonthlyRenewing = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, - subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationMonthlyRenewing) + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationMonthlyRenewing, + refundRequestStatusMessage: "Refund granted successfully!") ManageSubscriptionsView(viewModel: viewModelMonthlyRenewing) .previewDisplayName("Monthly renewing") From 2a9cd2c47de6882284d4616c937a27506db09f97 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Thu, 18 Jul 2024 17:31:50 +0200 Subject: [PATCH 10/90] [CustomerCenter] Add action handler (#4057) ### Description This PR provides a possible approach to implementing an "action" handler. This allows developer to respond to events that happen during the customer support flow. The current approach consists of making `CustomerCenterActionHandler`, a lambda that receives an action that can be passed in by the developer. Then calling that with the appropriate action from the customer center. This PR also moves some code to the view model for simplicity and moving logic away from the view layer. --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../Data/CustomerCenterAction.swift | 22 +++++++++++ .../View+PresentCustomerCenter.swift | 16 +++++--- .../ViewModels/CustomerCenterViewModel.swift | 37 ++++++++++++++++--- .../ManageSubscriptionsViewModel.swift | 21 ++++++++--- .../Views/CustomerCenterView.swift | 15 +++++--- .../Views/ManageSubscriptionsView.swift | 10 +++-- .../Views/RestorePurchasesAlert.swift | 15 ++------ .../CustomerCenterViewModelTests.swift | 24 +++++++----- .../ManageSubscriptionsViewModelTests.swift | 27 +++++++++----- .../UI/Views/SamplePaywallsList.swift | 28 +++++++++++++- 11 files changed, 164 insertions(+), 55 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 3fd53ba65d..0158e603bd 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1E473B682AC43254008B07F9 /* StoreMessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E473B672AC43254008B07F9 /* StoreMessageType.swift */; }; 1E568B512ACC6A8300D3C12F /* StoreMessageTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E568B502ACC6A8300D3C12F /* StoreMessageTypeTests.swift */; }; 1E5F8F6E2C4515430041EECD /* View+PresentCustomerCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */; }; + 1E5F8F782C46BBD90041EECD /* CustomerCenterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */; }; 1E99F81F2AC5917F0023E26E /* StoreMessagesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */; }; 2C0B98CD2797070B00C5874F /* PromotionalOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */; }; 2C6CC1162B8D2B6900432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6CC1152B8D2B6800432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift */; }; @@ -1049,6 +1050,7 @@ 1E473B692AC46908008B07F9 /* MockStoreMessagesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreMessagesHelper.swift; sourceTree = ""; }; 1E568B502ACC6A8300D3C12F /* StoreMessageTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessageTypeTests.swift; sourceTree = ""; }; 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+PresentCustomerCenter.swift"; sourceTree = ""; }; + 1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterAction.swift; sourceTree = ""; }; 1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessagesHelperTests.swift; sourceTree = ""; }; 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOffer.swift; sourceTree = ""; }; 2C646C282A0EBD0300E5936E /* CI-Snapshots.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "CI-Snapshots.xctestplan"; path = "Tests/TestPlans/CI-Snapshots.xctestplan"; sourceTree = ""; }; @@ -2557,6 +2559,7 @@ 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */, 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */, + 1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */, ); path = Data; sourceTree = ""; @@ -5162,6 +5165,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E5F8F782C46BBD90041EECD /* CustomerCenterAction.swift in Sources */, 887A60CC2C1D037000E1A461 /* PaywallFontProvider.swift in Sources */, 887A60B82C1D037000E1A461 /* Template1View.swift in Sources */, 887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift new file mode 100644 index 0000000000..18e38ee21c --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift @@ -0,0 +1,22 @@ +import RevenueCat + +/// Typealias for handler for Customer center actions +public typealias CustomerCenterActionHandler = @MainActor @Sendable (CustomerCenterAction) -> Void + +/// Represents an event the customer may perform during the Customer Center flow +public enum CustomerCenterAction { + + /// Starting the restoration process + case restoreStarted + /// Restore errored out + case restoreFailed(_ error: Error) + /// Restore completed successfully + case restoreCompleted(_ customerInfo: CustomerInfo) + /// Going to display manage subscription page, whether for cancellation or changing plans. + case showingManageSubscriptions + /// Starting refund request process + case refundRequestStarted(_ productId: String) + /// Refund request process finished, with result provided. + case refundRequestCompleted(_ refundRequestStatus: RefundRequestStatus) + +} diff --git a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift index 14015ec5ef..10082097a2 100644 --- a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift +++ b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift @@ -52,16 +52,19 @@ extension View { /// - Parameter isPresented: Binding indicating whether the customer center should be displayed /// - Parameter onDismiss: Callback executed when the customer center wants to be dismissed. /// Make sure you stop presenting the customer center when this is called + /// - Parameter customerCenterActionHandler: Allows to listen to certain events during the customer center flow. /// - Parameter presentationMode: The desired presentation mode of the customer center. Defaults to `.sheet`. public func presentCustomerCenter( isPresented: Binding, - onDismiss: @escaping () -> Void, - presentationMode: CustomerCenterPresentationMode = .default + customerCenterActionHandler: CustomerCenterActionHandler? = nil, + presentationMode: CustomerCenterPresentationMode = .default, + onDismiss: @escaping () -> Void ) -> some View { return self.modifier(PresentingCustomerCenterModifier( isPresented: isPresented, onDismiss: onDismiss, myAppPurchaseLogic: nil, + customerCenterActionHandler: customerCenterActionHandler, presentationMode: presentationMode )) } @@ -75,19 +78,22 @@ extension View { @available(visionOS, unavailable) private struct PresentingCustomerCenterModifier: ViewModifier { - var presentationMode: CustomerCenterPresentationMode - var onDismiss: (() -> Void) + let customerCenterActionHandler: CustomerCenterActionHandler? + let presentationMode: CustomerCenterPresentationMode + let onDismiss: (() -> Void) init( isPresented: Binding, onDismiss: @escaping () -> Void, myAppPurchaseLogic: MyAppPurchaseLogic?, + customerCenterActionHandler: CustomerCenterActionHandler?, presentationMode: CustomerCenterPresentationMode, purchaseHandler: PurchaseHandler? = nil ) { self._isPresented = isPresented self.presentationMode = presentationMode self.onDismiss = onDismiss + self.customerCenterActionHandler = customerCenterActionHandler self._purchaseHandler = .init(wrappedValue: purchaseHandler ?? PurchaseHandler.default(performPurchase: myAppPurchaseLogic?.performPurchase, performRestore: myAppPurchaseLogic?.performRestore)) @@ -117,7 +123,7 @@ private struct PresentingCustomerCenterModifier: ViewModifier { } private func customerCenterView() -> some View { - CustomerCenterView() + CustomerCenterView(customerCenterActionHandler: self.customerCenterActionHandler) .interactiveDismissDisabled(self.purchaseHandler.actionInProgress) } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 2a7a47ddb8..08f35e9bfd 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -16,6 +16,8 @@ import Foundation import RevenueCat +#if os(iOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -46,11 +48,13 @@ import RevenueCat } private var customerInfoFetcher: CustomerInfoFetcher + internal let customerCenterActionHandler: CustomerCenterActionHandler? private var error: Error? - convenience init() { - self.init(customerInfoFetcher: { + convenience init(customerCenterActionHandler: CustomerCenterActionHandler?) { + self.init(customerCenterActionHandler: customerCenterActionHandler, + customerInfoFetcher: { guard Purchases.isConfigured else { throw PaywallError.purchasesNotConfigured } @@ -59,14 +63,17 @@ import RevenueCat }) } - // @PublicForExternalTesting - init(customerInfoFetcher: @escaping CustomerInfoFetcher) { + init(customerCenterActionHandler: CustomerCenterActionHandler?, + customerInfoFetcher: @escaping CustomerInfoFetcher) { self.state = .notLoaded self.customerInfoFetcher = customerInfoFetcher + self.customerCenterActionHandler = customerCenterActionHandler } - // @PublicForExternalTesting - init(hasSubscriptions: Bool = false, areSubscriptionsFromApple: Bool = false) { + #if DEBUG + + init(hasSubscriptions: Bool = false, + areSubscriptionsFromApple: Bool = false) { self.hasSubscriptions = hasSubscriptions self.subscriptionsAreFromApple = areSubscriptionsFromApple self.customerInfoFetcher = { @@ -77,8 +84,11 @@ import RevenueCat return try await Purchases.shared.customerInfo() } self.state = .success + self.customerCenterActionHandler = nil } + #endif + func loadHasSubscriptions() async { do { // swiftlint:disable:next todo @@ -107,4 +117,19 @@ import RevenueCat } } + func performRestore() async -> RestorePurchasesAlert.AlertType { + self.customerCenterActionHandler?(.restoreStarted) + do { + let customerInfo = try await Purchases.shared.restorePurchases() + self.customerCenterActionHandler?(.restoreCompleted(customerInfo)) + let hasEntitlements = customerInfo.entitlements.active.count > 0 + return hasEntitlements ? .purchasesRecovered : .purchasesNotFound + } catch { + self.customerCenterActionHandler?(.restoreFailed(error)) + return .purchasesNotFound + } + } + } + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 9fe4e5c911..6cf9e28d7b 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -49,29 +49,36 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published private(set) var refundRequestStatusMessage: String? - private var purchasesProvider: ManageSubscriptionsPurchaseType + private let purchasesProvider: ManageSubscriptionsPurchaseType + private let customerCenterActionHandler: CustomerCenterActionHandler? private var error: Error? - convenience init(screen: CustomerCenterConfigData.Screen) { + convenience init(screen: CustomerCenterConfigData.Screen, + customerCenterActionHandler: CustomerCenterActionHandler?) { self.init(screen: screen, - purchasesProvider: ManageSubscriptionPurchases()) + purchasesProvider: ManageSubscriptionPurchases(), + customerCenterActionHandler: customerCenterActionHandler) } init(screen: CustomerCenterConfigData.Screen, - purchasesProvider: ManageSubscriptionsPurchaseType) { + purchasesProvider: ManageSubscriptionsPurchaseType, + customerCenterActionHandler: CustomerCenterActionHandler?) { self.state = .notLoaded self.screen = screen self.purchasesProvider = purchasesProvider + self.customerCenterActionHandler = customerCenterActionHandler } init(screen: CustomerCenterConfigData.Screen, subscriptionInformation: SubscriptionInformation, + customerCenterActionHandler: CustomerCenterActionHandler?, refundRequestStatusMessage: String? = nil) { self.screen = screen self.subscriptionInformation = subscriptionInformation self.purchasesProvider = ManageSubscriptionPurchases() self.refundRequestStatusMessage = refundRequestStatusMessage + self.customerCenterActionHandler = customerCenterActionHandler state = .success } @@ -141,7 +148,9 @@ class ManageSubscriptionsViewModel: ObservableObject { do { guard let subscriptionInformation = self.subscriptionInformation else { return } let productId = subscriptionInformation.productIdentifier - let status = try await purchasesProvider.beginRefundRequest(forProduct: productId) + self.customerCenterActionHandler?(.refundRequestStarted(productId)) + let status = try await self.purchasesProvider.beginRefundRequest(forProduct: productId) + self.customerCenterActionHandler?(.refundRequestCompleted(status)) switch status { case .error: self.refundRequestStatusMessage = String(localized: "Error when requesting refund, try again") @@ -151,11 +160,13 @@ class ManageSubscriptionsViewModel: ObservableObject { self.refundRequestStatusMessage = String(localized: "Refund canceled") } } catch { + self.customerCenterActionHandler?(.refundRequestCompleted(.error)) self.refundRequestStatusMessage = String(localized: "An error occurred while processing the refund request.") } case .changePlans, .cancel: do { + self.customerCenterActionHandler?(.showingManageSubscriptions) try await purchasesProvider.showManageSubscriptions() } catch { self.state = .error(error) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 853cd5ca8c..cf7728aeb3 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -26,10 +26,13 @@ import SwiftUI @available(visionOS, unavailable) public struct CustomerCenterView: View { - @StateObject private var viewModel = CustomerCenterViewModel() + @StateObject private var viewModel: CustomerCenterViewModel /// Create a view to handle common customer support tasks - public init() {} + public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil) { + self._viewModel = .init(wrappedValue: + CustomerCenterViewModel(customerCenterActionHandler: customerCenterActionHandler)) + } fileprivate init(viewModel: CustomerCenterViewModel) { self._viewModel = .init(wrappedValue: viewModel) @@ -38,10 +41,10 @@ public struct CustomerCenterView: View { // swiftlint:disable:next missing_docs public var body: some View { Group { - if !viewModel.isLoaded { + if !self.viewModel.isLoaded { ProgressView() } else { - if let configuration = viewModel.configuration { + if let configuration = self.viewModel.configuration { destinationView(configuration: configuration) } } @@ -49,6 +52,7 @@ public struct CustomerCenterView: View { .task { await loadInformationIfNeeded() } + .environmentObject(self.viewModel) } } @@ -72,7 +76,8 @@ private extension CustomerCenterView { if viewModel.hasSubscriptions { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { - ManageSubscriptionsView(screen: screen) + ManageSubscriptionsView(screen: screen, + customerCenterActionHandler: viewModel.customerCenterActionHandler) } else { WrongPlatformView() } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 525b6e3d49..dbd2b0ee95 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -31,8 +31,10 @@ struct ManageSubscriptionsView: View { @StateObject private var viewModel: ManageSubscriptionsViewModel - init(screen: CustomerCenterConfigData.Screen) { - let viewModel = ManageSubscriptionsViewModel(screen: screen) + init(screen: CustomerCenterConfigData.Screen, + customerCenterActionHandler: CustomerCenterActionHandler?) { + let viewModel = ManageSubscriptionsViewModel(screen: screen, + customerCenterActionHandler: customerCenterActionHandler) self._viewModel = .init(wrappedValue: viewModel) } @@ -262,13 +264,15 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { let viewModelMonthlyRenewing = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationMonthlyRenewing, + customerCenterActionHandler: nil, refundRequestStatusMessage: "Refund granted successfully!") ManageSubscriptionsView(viewModel: viewModelMonthlyRenewing) .previewDisplayName("Monthly renewing") let viewModelYearlyExpiring = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, - subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationYearlyExpiring) + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationYearlyExpiring, + customerCenterActionHandler: nil) ManageSubscriptionsView(viewModel: viewModelYearlyExpiring) .previewDisplayName("Yearly expiring") diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 0eff6cc2ce..1dea4e9e69 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -31,6 +31,8 @@ struct RestorePurchasesAlert: ViewModifier { @Environment(\.openURL) var openURL + @EnvironmentObject private var customerCenterViewModel: CustomerCenterViewModel + @State private var alertType: AlertType = .restorePurchases @Environment(\.dismiss) @@ -54,17 +56,8 @@ struct RestorePurchasesAlert: ViewModifier { """), primaryButton: .default(Text("Check past purchases"), action: { Task { - guard let customerInfo = try? await Purchases.shared.restorePurchases() else { - // todo: handle errors - self.setAlertType(.purchasesNotFound) - return - } - let hasEntitlements = customerInfo.entitlements.active.count > 0 - if hasEntitlements { - self.setAlertType(.purchasesRecovered) - } else { - self.setAlertType(.purchasesNotFound) - } + let alertType = await self.customerCenterViewModel.performRestore() + self.setAlertType(alertType) } }), secondaryButton: .cancel(Text("Cancel")) diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index cb715e048e..928a10ff94 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -37,7 +37,7 @@ class CustomerCenterViewModelTests: TestCase { } func testInitialState() { - let viewModel = CustomerCenterViewModel() + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil) expect(viewModel.state) == .notLoaded expect(viewModel.hasSubscriptions) == false @@ -46,7 +46,7 @@ class CustomerCenterViewModelTests: TestCase { } func testStateChangeToError() { - let viewModel = CustomerCenterViewModel() + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil) viewModel.state = .error(error) @@ -59,7 +59,7 @@ class CustomerCenterViewModelTests: TestCase { } func testIsLoaded() { - let viewModel = CustomerCenterViewModel() + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil) expect(viewModel.isLoaded) == false @@ -70,8 +70,9 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsApple() async { - let viewModel = CustomerCenterViewModel(customerInfoFetcher: { - return CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, + customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions }) await viewModel.loadHasSubscriptions() @@ -82,8 +83,9 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsGoogle() async { - let viewModel = CustomerCenterViewModel(customerInfoFetcher: { - return CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, + customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions }) await viewModel.loadHasSubscriptions() @@ -94,8 +96,9 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsNonActive() async { - let viewModel = CustomerCenterViewModel(customerInfoFetcher: { - return CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, + customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithoutSubscriptions }) await viewModel.loadHasSubscriptions() @@ -106,7 +109,8 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsFailure() async { - let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, + customerInfoFetcher: { throw TestError(message: "An error occurred") }) diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 04c8bc811e..2c6d391533 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -40,7 +40,8 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testInitialState() { - let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil) expect(viewModel.state) == CustomerCenterViewState.notLoaded expect(viewModel.subscriptionInformation).to(beNil()) @@ -51,7 +52,8 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testStateChangeToError() { - let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil) viewModel.state = CustomerCenterViewState.error(error) @@ -64,7 +66,8 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testIsLoaded() { - let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen) + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil) expect(viewModel.isLoaded) == false @@ -102,7 +105,8 @@ class ManageSubscriptionsViewModelTests: TestCase { purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products - )) + ), + customerCenterActionHandler: nil) // Act await viewModel.loadScreen() @@ -159,7 +163,8 @@ class ManageSubscriptionsViewModelTests: TestCase { purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products - )) + ), + customerCenterActionHandler: nil) // Act await viewModel.loadScreen() @@ -222,7 +227,8 @@ class ManageSubscriptionsViewModelTests: TestCase { purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products - )) + ), + customerCenterActionHandler: nil) // Act await viewModel.loadScreen() @@ -285,7 +291,8 @@ class ManageSubscriptionsViewModelTests: TestCase { purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products - )) + ), + customerCenterActionHandler: nil) // Act await viewModel.loadScreen() @@ -307,7 +314,8 @@ class ManageSubscriptionsViewModelTests: TestCase { let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: Fixtures.customerInfoWithoutSubscriptions - )) + ), + customerCenterActionHandler: nil) await viewModel.loadScreen() @@ -319,7 +327,8 @@ class ManageSubscriptionsViewModelTests: TestCase { let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, purchasesProvider: MockManageSubscriptionsPurchases( customerInfoError: error - )) + ), + customerCenterActionHandler: nil) await viewModel.loadScreen() diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index 617b8e9087..71d3fc657b 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -162,7 +162,7 @@ struct SamplePaywallsList: View { .frame(maxWidth: .infinity) .buttonStyle(.plain) #if os(iOS) - .presentCustomerCenter(isPresented: self.$presentingCustomerCenter) { + .presentCustomerCenter(isPresented: self.$presentingCustomerCenter, customerCenterActionHandler: self.handleCustomerCenterAction) { self.presentingCustomerCenter = false } #endif @@ -207,6 +207,32 @@ private struct TemplateLabel: View { // MARK: - +#if os(iOS) + +extension SamplePaywallsList { + + func handleCustomerCenterAction(action: CustomerCenterAction) { + switch action { + case .restoreCompleted(_): + print("CustomerCenter: restoreCompleted") + case .purchaseCompleted(_): + print("CustomerCenter: purchaseCompleted") + case .restoreStarted: + print("CustomerCenter: restoreStarted") + case .restoreFailed(_): + print("CustomerCenter: restoreFailed") + case .showingManageSubscriptions: + print("CustomerCenter: showingManageSubscriptions") + case .refundRequestStarted(let productId): + print("CustomerCenter: refundRequestStarted. ProductId: \(productId)") + case .refundRequestCompleted(let status): + print("CustomerCenter: refundRequestCompleted. Result: \(status)") + } + } +} + +#endif + private extension SamplePaywallsList { enum Display { From fa063240876909645e48e09d0a701554b289d10d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 19 Jul 2024 16:30:09 +0200 Subject: [PATCH 11/90] [Customer Center] Promotional Offers support (#3968) Adds displaying a Promotional Offer if the path has an offer id configured --------- Co-authored-by: JayShortway <29483617+JayShortway@users.noreply.github.com> Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com> --- RevenueCat.xcodeproj/project.pbxproj | 46 ++++- .../CustomerCenterPurchasesType.swift | 32 +++ .../ManageSubscriptionsPurchaseType.swift | 0 .../CustomerInfo+CurrentEntitlement.swift | 35 ++++ .../Data/CustomerCenterConfigTestData.swift | 16 +- .../Data/CustomerCenterEnvironment.swift | 38 ++++ .../Data/CustomerCenterError.swift | 5 + .../Data/CustomerCenterPurchases.swift | 37 ++++ .../Data/FeedbackSurveyData.swift | 7 +- .../Data/LoadPromotionalOfferUseCase.swift | 74 +++++++ .../Data/PromotionalOfferData.swift | 24 +++ .../ViewModels/FeedbackSurveyViewModel.swift | 74 +++++++ .../ManageSubscriptionsViewModel.swift | 80 +++++--- .../PromotionalOfferViewModel.swift | 76 +++++++ .../Views/CustomerCenterView.swift | 14 +- .../Views/FeedbackSurveyView.swift | 40 +++- .../Views/ManageSubscriptionsView.swift | 61 ++++-- .../Views/PromotionalOfferView.swift | 182 +++++++++++++++++ RevenueCatUI/Data/Strings.swift | 15 +- .../Resources/en.lproj/Localizable.strings | 1 + .../Resources/en_AU.lproj/Localizable.strings | 1 + .../Resources/en_CA.lproj/Localizable.strings | 1 + .../Resources/en_GB.lproj/Localizable.strings | 1 + .../Resources/en_US.lproj/Localizable.strings | 1 + .../es_419.lproj/Localizable.strings | 1 + .../Resources/es_ES.lproj/Localizable.strings | 1 + .../CustomerCenterConfigData.swift | 35 +++- .../CustomerCenterConfigResponse.swift | 2 + .../CustomerCenterConfigDataAPI.swift | 12 +- .../ManageSubscriptionsViewModelTests.swift | 187 ++++++++++++++++-- .../CustomerCenterConfigDataTests.swift | 9 +- .../BackendGetCustomerCenterConfigTests.swift | 12 +- .../iOS13-testGetCustomerCenterConfig.1.json | 25 +++ ...omerCenterConfigCachesForSameUserID.1.json | 25 +++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 25 +++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 25 +++ ...GetCustomerCenterConfigFailSendsNil.1.json | 25 +++ ...rCenterConfigNetworkErrorSendsError.1.json | 25 +++ ...etCustomerCenterConfigPassesLocales.1.json | 25 +++ ...rConfigDoesntCacheForMultipleUserID.1.json | 25 +++ ...rConfigDoesntCacheForMultipleUserID.2.json | 25 +++ ...testRepeatedRequestsLogDebugMessage.1.json | 25 +++ .../iOS16-testGetCustomerCenterConfig.1.json | 25 +++ ...omerCenterConfigCachesForSameUserID.1.json | 25 +++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 25 +++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 25 +++ ...GetCustomerCenterConfigFailSendsNil.1.json | 25 +++ ...rCenterConfigNetworkErrorSendsError.1.json | 25 +++ ...etCustomerCenterConfigPassesLocales.1.json | 25 +++ ...rConfigDoesntCacheForMultipleUserID.1.json | 25 +++ ...rConfigDoesntCacheForMultipleUserID.2.json | 25 +++ ...testRepeatedRequestsLogDebugMessage.1.json | 25 +++ ...DiagnosticsEventsWithMultipleEvents.1.json | 44 +++++ ...stPostDiagnosticsEventsWithOneEvent.1.json | 36 ++++ 54 files changed, 1615 insertions(+), 85 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift rename RevenueCatUI/CustomerCenter/{ => Abstractions}/ManageSubscriptionsPurchaseType.swift (100%) create mode 100644 RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testRepeatedRequestsLogDebugMessage.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 0158e603bd..dcc74dac1a 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -130,6 +130,7 @@ 2DFF6C56270CA28800ECAFAB /* MockRequestFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */; }; 35109DAB2BC6E436001030C8 /* BackendPostDiagnosticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35109DAA2BC6E436001030C8 /* BackendPostDiagnosticsTests.swift */; }; 35109DB92BC8143E001030C8 /* DiagnosticsEventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35109DB82BC8143E001030C8 /* DiagnosticsEventsRequest.swift */; }; + 3511088F2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */; }; 351B513D26D4491E00BD2BD7 /* MockDeviceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513C26D4491E00BD2BD7 /* MockDeviceCache.swift */; }; 351B513F26D4496000BD2BD7 /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513E26D4496000BD2BD7 /* MockIdentityManager.swift */; }; 351B514126D4498F00BD2BD7 /* MockBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514026D4498F00BD2BD7 /* MockBackend.swift */; }; @@ -203,8 +204,12 @@ 3543914526F926D900E669DF /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */; }; 3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */; }; 3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */; }; + 3546355C2C391F38001D7E85 /* FeedbackSurveyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3546355A2C391F38001D7E85 /* FeedbackSurveyViewModel.swift */; }; + 3546355D2C391F38001D7E85 /* PromotionalOfferViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3546355B2C391F38001D7E85 /* PromotionalOfferViewModel.swift */; }; + 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */; }; 354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */; }; 354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; }; + 3551E3AB2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; 3592E8862C2ED51700D7F91D /* CustomerCenterConfigCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */; }; @@ -224,6 +229,7 @@ 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */; }; 35C272A12BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; 35C272A22BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; + 35C496062C482ACC0023E924 /* PromotionalOfferData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C496052C482ACC0023E924 /* PromotionalOfferData.swift */; }; 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */; }; 35D159CB2BC4396F004D8061 /* DiagnosticsPostOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D159CA2BC4396F004D8061 /* DiagnosticsPostOperation.swift */; }; 35D159CF2BC43B89004D8061 /* DiagnosticsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D159CE2BC43B89004D8061 /* DiagnosticsSynchronizer.swift */; }; @@ -236,6 +242,9 @@ 35D83312262FBD4200E60AC5 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; }; 35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */; }; 35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; }; + 35F249CA2C493D970058993A /* LoadPromotionalOfferUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249C92C493D970058993A /* LoadPromotionalOfferUseCase.swift */; }; + 35F249CC2C493DCC0058993A /* CustomerCenterPurchasesType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249CB2C493DCC0058993A /* CustomerCenterPurchasesType.swift */; }; + 35F249CE2C493E3D0058993A /* CustomerCenterPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */; }; 35F38B482C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */; }; 35F82BAB26A84E130051DF03 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */; }; 35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */; }; @@ -1140,6 +1149,7 @@ 350A1B84226E3E8700CCA10F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 35109DAA2BC6E436001030C8 /* BackendPostDiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostDiagnosticsTests.swift; sourceTree = ""; }; 35109DB82BC8143E001030C8 /* DiagnosticsEventsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsEventsRequest.swift; sourceTree = ""; }; + 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerInfo+CurrentEntitlement.swift"; sourceTree = ""; }; 351B513C26D4491E00BD2BD7 /* MockDeviceCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceCache.swift; sourceTree = ""; }; 351B513E26D4496000BD2BD7 /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = ""; }; 351B514026D4498F00BD2BD7 /* MockBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBackend.swift; sourceTree = ""; }; @@ -1181,8 +1191,12 @@ 353756632C382C2800A1B8D6 /* URLUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLUtilities.swift; sourceTree = ""; }; 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = ""; }; 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsViewModelTests.swift; sourceTree = ""; }; + 3546355A2C391F38001D7E85 /* FeedbackSurveyViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyViewModel.swift; sourceTree = ""; }; + 3546355B2C391F38001D7E85 /* PromotionalOfferViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromotionalOfferViewModel.swift; sourceTree = ""; }; + 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromotionalOfferView.swift; sourceTree = ""; }; 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionKey.swift; sourceTree = ""; }; 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = ""; }; + 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterEnvironment.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; 357C9BC022725CFA006BC624 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/iAd.framework; sourceTree = DEVELOPER_DIR; }; @@ -1203,6 +1217,7 @@ 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyData.swift; sourceTree = ""; }; 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyView.swift; sourceTree = ""; }; 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsTracker.swift; sourceTree = ""; }; + 35C496052C482ACC0023E924 /* PromotionalOfferData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOfferData.swift; sourceTree = ""; }; 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorUtils.swift; sourceTree = ""; }; 35D159CA2BC4396F004D8061 /* DiagnosticsPostOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsPostOperation.swift; sourceTree = ""; }; 35D159CE2BC43B89004D8061 /* DiagnosticsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSynchronizer.swift; sourceTree = ""; }; @@ -1215,6 +1230,9 @@ 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK1Tests.swift; sourceTree = ""; }; 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsHelper.swift; sourceTree = ""; }; 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManageSubscriptionsHelper.swift; sourceTree = ""; }; + 35F249C92C493D970058993A /* LoadPromotionalOfferUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadPromotionalOfferUseCase.swift; sourceTree = ""; }; + 35F249CB2C493DCC0058993A /* CustomerCenterPurchasesType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPurchasesType.swift; sourceTree = ""; }; + 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPurchases.swift; sourceTree = ""; }; 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendGetCustomerCenterConfigTests.swift; sourceTree = ""; }; 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = ""; }; 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionDataMigratorTests.swift; sourceTree = ""; }; @@ -2560,6 +2578,10 @@ 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */, 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */, 1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */, + 35C496052C482ACC0023E924 /* PromotionalOfferData.swift */, + 35F249C92C493D970058993A /* LoadPromotionalOfferUseCase.swift */, + 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */, + 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */, ); path = Data; sourceTree = ""; @@ -2570,6 +2592,8 @@ 353756572C382C2800A1B8D6 /* CustomerCenterViewModel.swift */, 353756582C382C2800A1B8D6 /* CustomerCenterViewState.swift */, 353756592C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift */, + 3546355A2C391F38001D7E85 /* FeedbackSurveyViewModel.swift */, + 3546355B2C391F38001D7E85 /* PromotionalOfferViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -2581,6 +2605,7 @@ 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */, 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */, 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */, + 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */, 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */, ); @@ -2590,13 +2615,14 @@ 353756642C382C2800A1B8D6 /* CustomerCenter */ = { isa = PBXGroup; children = ( + 35653BD32C46803A009E8ADB /* Abstractions */, 353756562C382C2800A1B8D6 /* Data */, 3537565A2C382C2800A1B8D6 /* ViewModels */, 353756602C382C2800A1B8D6 /* Views */, 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */, - 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */, 353756632C382C2800A1B8D6 /* URLUtilities.swift */, 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */, + 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */, ); path = CustomerCenter; sourceTree = ""; @@ -2691,6 +2717,15 @@ path = SubscriberAttributes; sourceTree = ""; }; + 35653BD32C46803A009E8ADB /* Abstractions */ = { + isa = PBXGroup; + children = ( + 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */, + 35F249CB2C493DCC0058993A /* CustomerCenterPurchasesType.swift */, + ); + path = Abstractions; + sourceTree = ""; + }; 357348FE2C3BEAF8000EEB86 /* CustomerCenter */ = { isa = PBXGroup; children = ( @@ -5177,6 +5212,7 @@ 887A60722C1D037000E1A461 /* PaywallViewMode+Extensions.swift in Sources */, 887A60C32C1D037000E1A461 /* FooterView.swift in Sources */, 887A607F2C1D037000E1A461 /* Optional+Extensions.swift in Sources */, + 3511088F2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift in Sources */, 887A60752C1D037000E1A461 /* TemplateViewConfiguration.swift in Sources */, 887A60702C1D037000E1A461 /* PaywallTemplate.swift in Sources */, 887A60882C1D037000E1A461 /* MockPurchases.swift in Sources */, @@ -5186,6 +5222,7 @@ 887A60C92C1D037000E1A461 /* PurchaseButton.swift in Sources */, 887A60812C1D037000E1A461 /* PaywallData+Default.swift in Sources */, 887A606A2C1D037000E1A461 /* TrialOrIntroEligibilityChecker.swift in Sources */, + 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */, 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */, 887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */, 887A60C02C1D037000E1A461 /* AsyncButton.swift in Sources */, @@ -5199,6 +5236,7 @@ 353756682C382C2800A1B8D6 /* CustomerCenterViewModel.swift in Sources */, 887A60BD2C1D037000E1A461 /* TemplateViewType.swift in Sources */, 887A606D2C1D037000E1A461 /* Localization.swift in Sources */, + 3551E3AB2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift in Sources */, 887A60CA2C1D037000E1A461 /* RemoteImage.swift in Sources */, 887A607B2C1D037000E1A461 /* Bundle+Extensions.swift in Sources */, 353756662C382C2800A1B8D6 /* CustomerCenterError.swift in Sources */, @@ -5208,8 +5246,10 @@ 887A607E2C1D037000E1A461 /* Logger.swift in Sources */, 887A60852C1D037000E1A461 /* FitToAspectRatio.swift in Sources */, 353756652C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift in Sources */, + 3546355C2C391F38001D7E85 /* FeedbackSurveyViewModel.swift in Sources */, 353756692C382C2800A1B8D6 /* CustomerCenterViewState.swift in Sources */, 887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */, + 35F249CE2C493E3D0058993A /* CustomerCenterPurchases.swift in Sources */, 353756672C382C2800A1B8D6 /* SubscriptionInformation.swift in Sources */, 887A60682C1D037000E1A461 /* TemplateError.swift in Sources */, 887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */, @@ -5224,8 +5264,10 @@ 887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */, 887A60782C1D037000E1A461 /* TestData.swift in Sources */, 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */, + 35F249CC2C493DCC0058993A /* CustomerCenterPurchasesType.swift in Sources */, 887A60672C1D037000E1A461 /* PaywallError.swift in Sources */, 88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */, + 35C496062C482ACC0023E924 /* PromotionalOfferData.swift in Sources */, 887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */, 887A60802C1D037000E1A461 /* Package+VariableDataProvider.swift in Sources */, 887A60CE2C1D037000E1A461 /* View+PresentPaywall.swift in Sources */, @@ -5249,6 +5291,8 @@ 887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */, 887A606F2C1D037000E1A461 /* PaywallData+Validation.swift in Sources */, 88A543DF2C37A45B0039C6A5 /* TemplatePackageSetting.swift in Sources */, + 3546355D2C391F38001D7E85 /* PromotionalOfferViewModel.swift in Sources */, + 35F249CA2C493D970058993A /* LoadPromotionalOfferUseCase.swift in Sources */, 887A60762C1D037000E1A461 /* TemplateViewConfiguration+Extensions.swift in Sources */, 887A607A2C1D037000E1A461 /* Variables.swift in Sources */, ); diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift new file mode 100644 index 0000000000..3fbce0a20d --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -0,0 +1,32 @@ +// +// 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 +// +// CustomerCenterPurchaseType.swift +// +// Created by Cesar de la Vega on 18/7/24. + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +protocol CustomerCenterPurchasesType: Sendable { + + @Sendable + func customerInfo() async throws -> CustomerInfo + + @Sendable + func products(_ productIdentifiers: [String]) async -> [StoreProduct] + + func promotionalOffer(forProductDiscount discount: StoreProductDiscount, + product: StoreProduct) async throws -> PromotionalOffer + +} diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift similarity index 100% rename from RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift rename to RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift diff --git a/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift b/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift new file mode 100644 index 0000000000..cb9594961d --- /dev/null +++ b/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift @@ -0,0 +1,35 @@ +// +// 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 +// +// CustomerInfo+CurrentEntitlement.swift +// +// Created by Cesar de la Vega on 17/7/24. + +import Foundation +import RevenueCat + +extension CustomerInfo { + + /// Returns the earliest expiring iOS App Store entitlement. If this CustomerInfo contains multiple lifetime + /// entitlements and no expiring entitlements, the returned entitlement is undefined. + func earliestExpiringAppStoreEntitlement() -> EntitlementInfo? { + return self.entitlements + .active + .values + .lazy + .filter { $0.store == .appStore } + .sorted { lhs, rhs in + let lhsDateSeconds = lhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude + let rhsDateSeconds = rhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude + return lhsDateSeconds < rhsDateSeconds + } + .first + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index b846d75128..6be73b9494 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -36,7 +36,12 @@ enum CustomerCenterConfigTestData { id: "2", title: "Request a refund", type: .refundRequest, - detail: nil + detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer( + iosOfferId: "offer_id", + eligible: true, + title: "title", + subtitle: "subtitle" + )) ), .init( id: "3", @@ -53,15 +58,18 @@ enum CustomerCenterConfigTestData { options: [ .init( id: "1", - title: "Too expensive" + title: "Too expensive", + promotionalOffer: nil ), .init( id: "2", - title: "Don't use the app" + title: "Don't use the app", + promotionalOffer: nil ), .init( id: "3", - title: "Bought by mistake" + title: "Bought by mistake", + promotionalOffer: nil ) ] )) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift new file mode 100644 index 0000000000..7cd36e9a53 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -0,0 +1,38 @@ +// +// 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 +// +// CustomerCenterEnvironment.swift +// +// Created by Cesar de la Vega on 19/7/24. + +import Foundation +import RevenueCat +import SwiftUI + +struct LocalizationKey: EnvironmentKey { + + static let defaultValue: CustomerCenterConfigData.Localization = .default + +} + +extension CustomerCenterConfigData.Localization { + + /// Default ``CustomerCenterConfigData.Localization`` value for Environment usage + public static let `default` = CustomerCenterConfigData.Localization(locale: "en_US", localizedStrings: [:]) + +} + +extension EnvironmentValues { + + var localization: CustomerCenterConfigData.Localization { + get { self[LocalizationKey.self] } + set { self[LocalizationKey.self] = newValue } + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift index af5a2e1bfa..8c8c7ab4d1 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -21,6 +21,9 @@ enum CustomerCenterError: Error { /// Could not find information for an active subscription. case couldNotFindSubscriptionInformation + /// Could not find offer id for any active product + case couldNotFindOfferForActiveProducts + } extension CustomerCenterError: CustomNSError { @@ -35,6 +38,8 @@ extension CustomerCenterError: CustomNSError { switch self { case .couldNotFindSubscriptionInformation: return "Could not find information for an active subscription." + case .couldNotFindOfferForActiveProducts: + return "Could not find any product with specified offer id." } } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift new file mode 100644 index 0000000000..ead0484128 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// CustomerCenterPurchases.swift +// +// Created by Cesar de la Vega on 18/7/24. + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +final class CustomerCenterPurchases: CustomerCenterPurchasesType { + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + try await Purchases.shared.customerInfo() + } + + func products(_ productIdentifiers: [String]) async -> [StoreProduct] { + await Purchases.shared.products(productIdentifiers) + } + + func promotionalOffer(forProductDiscount discount: StoreProductDiscount, + product: StoreProduct) async throws -> PromotionalOffer { + try await Purchases.shared.promotionalOffer(forProductDiscount: discount, + product: product) + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift index 8797525399..c7e85c0fe3 100644 --- a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift +++ b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift @@ -25,11 +25,12 @@ import RevenueCat class FeedbackSurveyData: ObservableObject { var configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey - var action: (() -> Void) + var onOptionSelected: (() -> Void) - init(configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey, action: @escaping (() -> Void)) { + init(configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey, + onOptionSelected: @escaping (() -> Void)) { self.configuration = configuration - self.action = action + self.onOptionSelected = onOptionSelected } } diff --git a/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift new file mode 100644 index 0000000000..45073dcf66 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift @@ -0,0 +1,74 @@ +// +// 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 +// +// LoadPromotionalOfferUseCase.swift +// +// Created by Cesar de la Vega on 18/7/24. + +import Foundation +import RevenueCat + +protocol LoadPromotionalOfferUseCaseType { + + func execute( + promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer + ) async -> Result + +} + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +@MainActor +class LoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType { + + private let purchasesProvider: CustomerCenterPurchasesType + + init(purchasesProvider: CustomerCenterPurchasesType = CustomerCenterPurchases()) { + self.purchasesProvider = purchasesProvider + } + + func execute( + promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer + ) async -> Result { + do { + let customerInfo = try await self.purchasesProvider.customerInfo() + + guard let productIdentifier = customerInfo.earliestExpiringAppStoreEntitlement()?.productIdentifier, + let subscribedProduct = await self.purchasesProvider.products([productIdentifier]).first else { + Logger.warning(Strings.could_not_offer_for_active_subscriptions) + return .failure(CustomerCenterError.couldNotFindSubscriptionInformation) + } + + guard let discount = subscribedProduct.discounts.first(where: { + $0.offerIdentifier == promoOfferDetails.iosOfferId + }) else { + Logger.warning(Strings.could_not_offer_for_active_subscriptions) + return .failure(CustomerCenterError.couldNotFindSubscriptionInformation) + } + + let promotionalOffer = try await self.purchasesProvider.promotionalOffer(forProductDiscount: discount, + product: subscribedProduct) + let promotionalOfferData = PromotionalOfferData(promotionalOffer: promotionalOffer, + product: subscribedProduct, + promoOfferDetails: promoOfferDetails) + return .success(promotionalOfferData) + } catch { + Logger.warning(Strings.error_fetching_promotional_offer(error)) + return .failure(CustomerCenterError.couldNotFindOfferForActiveProducts) + } + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift b/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift new file mode 100644 index 0000000000..54b6621c5c --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift @@ -0,0 +1,24 @@ +// +// 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 +// +// PromotionalOfferData.swift +// +// Created by Cesar de la Vega on 17/7/24. + +import Foundation +import RevenueCat + +struct PromotionalOfferData: Identifiable { + + let id = UUID() + let promotionalOffer: PromotionalOffer + let product: StoreProduct + let promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift new file mode 100644 index 0000000000..42b2ddced9 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift @@ -0,0 +1,74 @@ +// +// 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 +// +// FeedbackSurveyViewModel.swift +// +// +// Created by Cesar de la Vega on 17/6/24. +// + +import Foundation +import RevenueCat + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class FeedbackSurveyViewModel: ObservableObject { + + var feedbackSurveyData: FeedbackSurveyData + + @Published + var loadingState: String? + @Published + var promotionalOfferData: PromotionalOfferData? + + private var purchasesProvider: CustomerCenterPurchasesType + private let loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType + + convenience init(feedbackSurveyData: FeedbackSurveyData) { + self.init(feedbackSurveyData: feedbackSurveyData, + purchasesProvider: CustomerCenterPurchases(), + loadPromotionalOfferUseCase: LoadPromotionalOfferUseCase()) + } + + init(feedbackSurveyData: FeedbackSurveyData, + purchasesProvider: CustomerCenterPurchasesType, + loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType) { + self.feedbackSurveyData = feedbackSurveyData + self.purchasesProvider = purchasesProvider + self.loadPromotionalOfferUseCase = loadPromotionalOfferUseCase + } + + func handleAction(for option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async { + if let promotionalOffer = option.promotionalOffer { + self.loadingState = option.id + let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promotionalOffer) + switch result { + case .success(let promotionalOfferData): + self.promotionalOfferData = promotionalOfferData + case .failure: + self.feedbackSurveyData.onOptionSelected() + } + } else { + self.feedbackSurveyData.onOptionSelected() + } + } + + func handleSheetDismiss() { + self.feedbackSurveyData.onOptionSelected() + self.loadingState = nil + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 6cf9e28d7b..f88bdc4de5 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -32,6 +32,10 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var feedbackSurveyData: FeedbackSurveyData? + @Published + var loadingPath: CustomerCenterConfigData.HelpPath? + @Published + var promotionalOfferData: PromotionalOfferData? @Published var state: CustomerCenterViewState { didSet { @@ -40,6 +44,7 @@ class ManageSubscriptionsViewModel: ObservableObject { } } } + var isLoaded: Bool { return state != .notLoaded } @@ -49,25 +54,20 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published private(set) var refundRequestStatusMessage: String? - private let purchasesProvider: ManageSubscriptionsPurchaseType + private var purchasesProvider: ManageSubscriptionsPurchaseType + private let loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType private let customerCenterActionHandler: CustomerCenterActionHandler? - private var error: Error? - convenience init(screen: CustomerCenterConfigData.Screen, - customerCenterActionHandler: CustomerCenterActionHandler?) { - self.init(screen: screen, - purchasesProvider: ManageSubscriptionPurchases(), - customerCenterActionHandler: customerCenterActionHandler) - } - init(screen: CustomerCenterConfigData.Screen, - purchasesProvider: ManageSubscriptionsPurchaseType, - customerCenterActionHandler: CustomerCenterActionHandler?) { - self.state = .notLoaded + customerCenterActionHandler: CustomerCenterActionHandler?, + purchasesProvider: ManageSubscriptionsPurchaseType = ManageSubscriptionPurchases(), + loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType? = nil) { self.screen = screen self.purchasesProvider = purchasesProvider self.customerCenterActionHandler = customerCenterActionHandler + self.loadPromotionalOfferUseCase = loadPromotionalOfferUseCase ?? LoadPromotionalOfferUseCase() + self.state = .notLoaded } init(screen: CustomerCenterConfigData.Screen, @@ -79,6 +79,7 @@ class ManageSubscriptionsViewModel: ObservableObject { self.purchasesProvider = ManageSubscriptionPurchases() self.refundRequestStatusMessage = refundRequestStatusMessage self.customerCenterActionHandler = customerCenterActionHandler + self.loadPromotionalOfferUseCase = LoadPromotionalOfferUseCase() state = .success } @@ -94,18 +95,7 @@ class ManageSubscriptionsViewModel: ObservableObject { private func loadSubscriptionInformation() async throws { let customerInfo = try await purchasesProvider.customerInfo() - // Pick the soonest expiring iOS App Store entitlement and accompanying product. - guard let currentEntitlement = customerInfo.entitlements - .active - .values - .lazy - .filter({ entitlement in entitlement.store == .appStore }) - .sorted(by: { lhs, rhs in - let lhsDateSeconds = lhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude - let rhsDateSeconds = rhs.expirationDate?.timeIntervalSince1970 ?? TimeInterval.greatestFiniteMagnitude - - return lhsDateSeconds < rhsDateSeconds - }).first, + guard let currentEntitlement = customerInfo.earliestExpiringAppStoreEntitlement(), let subscribedProduct = await purchasesProvider.products([currentEntitlement.productIdentifier]).first else { Logger.warning(Strings.could_not_find_subscription_information) @@ -127,20 +117,48 @@ class ManageSubscriptionsViewModel: ObservableObject { ) } - #if os(iOS) || targetEnvironment(macCatalyst) +#if os(iOS) || targetEnvironment(macCatalyst) func determineFlow(for path: CustomerCenterConfigData.HelpPath) async { - if case let .feedbackSurvey(feedbackSurvey) = path.detail { + switch path.detail { + case let .feedbackSurvey(feedbackSurvey): self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in Task { - await self?.performAction(for: path) + await self?.onPathSelected(path: path) } } - } else { - await self.performAction(for: path) + case let .promotionalOffer(promotionalOffer): + self.loadingPath = path + let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promotionalOffer) + switch result { + case .success(let promotionalOfferData): + self.promotionalOfferData = promotionalOfferData + case .failure: + await self.onPathSelected(path: path) + self.loadingPath = nil + } + default: + await self.onPathSelected(path: path) } } - func performAction(for path: CustomerCenterConfigData.HelpPath) async { + func handleSheetDismiss() async { + if let loadingPath = loadingPath { + await self.onPathSelected(path: loadingPath) + self.loadingPath = nil + } + } +#endif + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private extension ManageSubscriptionsViewModel { + +#if os(iOS) || targetEnvironment(macCatalyst) + private func onPathSelected(path: CustomerCenterConfigData.HelpPath) async { switch path.type { case .missingPurchase: self.showRestoreAlert = true @@ -175,7 +193,7 @@ class ManageSubscriptionsViewModel: ObservableObject { break } } - #endif +#endif } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift new file mode 100644 index 0000000000..6171ff3a17 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift @@ -0,0 +1,76 @@ +// +// 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 +// +// PromotionalOfferViewModel.swift +// +// +// Created by Cesar de la Vega on 17/6/24. +// + +import Foundation +import RevenueCat + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +@MainActor +class PromotionalOfferViewModel: ObservableObject { + + @Published + private(set) var promotionalOfferData: PromotionalOfferData? + @Published + private(set) var error: Error? + + private var purchasesProvider: CustomerCenterPurchasesType + private let loadPromotionalOfferUseCase: LoadPromotionalOfferUseCase + + convenience init() { + self.init(promotionalOfferData: nil) + } + + init(promotionalOfferData: PromotionalOfferData?) { + self.promotionalOfferData = promotionalOfferData + self.purchasesProvider = CustomerCenterPurchases() + self.loadPromotionalOfferUseCase = LoadPromotionalOfferUseCase() + } + + func purchasePromo() async { + guard let promotionalOffer = self.promotionalOfferData?.promotionalOffer, + let product = self.promotionalOfferData?.product else { + Logger.warning(Strings.promo_offer_not_loaded) + return + } + + do { + let result = try await Purchases.shared.purchase(product: product, promotionalOffer: promotionalOffer) + // swiftlint:disable:next todo + // TODO: do something with result + Logger.debug("Purchased promotional offer: \(result)") + } catch { + self.error = error + } + } + + func loadPromo(promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer) async { + let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promoOfferDetails) + switch result { + case .success(let promotionalOfferData): + self.promotionalOfferData = promotionalOfferData + case .failure(let error): + self.error = error + } + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index cf7728aeb3..6117e01742 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -28,14 +28,20 @@ public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel + private var localization: CustomerCenterConfigData.Localization + /// Create a view to handle common customer support tasks - public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil) { + public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil, + localization: CustomerCenterConfigData.Localization = .default) { self._viewModel = .init(wrappedValue: CustomerCenterViewModel(customerCenterActionHandler: customerCenterActionHandler)) + self.localization = localization } - fileprivate init(viewModel: CustomerCenterViewModel) { + fileprivate init(viewModel: CustomerCenterViewModel, + localization: CustomerCenterConfigData.Localization = .default) { self._viewModel = .init(wrappedValue: viewModel) + self.localization = localization } // swiftlint:disable:next missing_docs @@ -46,6 +52,7 @@ public struct CustomerCenterView: View { } else { if let configuration = self.viewModel.configuration { destinationView(configuration: configuration) + .environment(\.localization, configuration.localization) } } } @@ -77,7 +84,8 @@ private extension CustomerCenterView { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { ManageSubscriptionsView(screen: screen, - customerCenterActionHandler: viewModel.customerCenterActionHandler) + customerCenterActionHandler: viewModel.customerCenterActionHandler, + localization: configuration.localization) } else { WrongPlatformView() } diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 921cc22f04..5522899d45 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -25,20 +25,37 @@ import SwiftUI @available(visionOS, unavailable) struct FeedbackSurveyView: View { - @ObservedObject - var feedbackSurveyData: FeedbackSurveyData + @StateObject + private var viewModel: FeedbackSurveyViewModel + + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + + init(feedbackSurveyData: FeedbackSurveyData) { + let viewModel = FeedbackSurveyViewModel(feedbackSurveyData: feedbackSurveyData) + self._viewModel = StateObject(wrappedValue: viewModel) + } var body: some View { VStack { - Text(feedbackSurveyData.configuration.title) + Text(self.viewModel.feedbackSurveyData.configuration.title) .font(.title) .padding() Spacer() - FeedbackSurveyButtonsView(options: feedbackSurveyData.configuration.options, - action: feedbackSurveyData.action) + FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options, + onOptionSelected: self.viewModel.handleAction(for:), + loadingState: self.$viewModel.loadingState) } + .sheet( + item: self.$viewModel.promotionalOfferData, + onDismiss: { self.viewModel.handleSheetDismiss() }, + content: { promotionalOfferData in + PromotionalOfferView(promotionalOffer: promotionalOfferData.promotionalOffer, + product: promotionalOfferData.product, + promoOfferDetails: promotionalOfferData.promoOfferDetails) + }) } } @@ -51,17 +68,24 @@ struct FeedbackSurveyView: View { struct FeedbackSurveyButtonsView: View { let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] - let action: (() -> Void) + let onOptionSelected: (_ optionSelected: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async -> Void + @Binding + var loadingState: String? var body: some View { VStack(spacing: Self.buttonSpacing) { ForEach(options, id: \.id) { option in AsyncButton(action: { - self.action() + await self.onOptionSelected(option) }, label: { - Text(option.title) + if self.loadingState == option.id { + ProgressView() + } else { + Text(option.title) + } }) .buttonStyle(ManageSubscriptionsButtonStyle()) + .disabled(self.loadingState != nil) } } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index dbd2b0ee95..12ef3777e7 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -31,8 +31,12 @@ struct ManageSubscriptionsView: View { @StateObject private var viewModel: ManageSubscriptionsViewModel + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + init(screen: CustomerCenterConfigData.Screen, - customerCenterActionHandler: CustomerCenterActionHandler?) { + customerCenterActionHandler: CustomerCenterActionHandler?, + localization: CustomerCenterConfigData.Localization) { let viewModel = ManageSubscriptionsViewModel(screen: screen, customerCenterActionHandler: customerCenterActionHandler) self._viewModel = .init(wrappedValue: viewModel) @@ -86,8 +90,8 @@ struct ManageSubscriptionsView: View { Spacer() - ManageSubscriptionsButtonsView(viewModel: self.viewModel) - + ManageSubscriptionsButtonsView(viewModel: self.viewModel, + loadingPath: self.$viewModel.loadingPath) } else { ProgressView() .progressViewStyle(CircularProgressViewStyle()) @@ -226,7 +230,9 @@ struct SubscriptionDetailsView: View { struct ManageSubscriptionsButtonsView: View { @ObservedObject - private(set) var viewModel: ManageSubscriptionsViewModel + var viewModel: ManageSubscriptionsViewModel + @Binding + var loadingPath: CustomerCenterConfigData.HelpPath? var body: some View { VStack(spacing: 16) { @@ -238,19 +244,50 @@ struct ManageSubscriptionsButtonsView: View { #endif } ForEach(filteredPaths, id: \.id) { path in - AsyncButton(action: { - await self.viewModel.determineFlow(for: path) - }, label: { - Text(path.title) - }) - .restorePurchasesAlert(isPresented: self.$viewModel.showRestoreAlert) - .buttonStyle(ManageSubscriptionsButtonStyle()) + ManageSubscriptionButton(path: path, viewModel: self.viewModel) } } } } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct ManageSubscriptionButton: View { + + let path: CustomerCenterConfigData.HelpPath + @ObservedObject var viewModel: ManageSubscriptionsViewModel + + var body: some View { + AsyncButton(action: { + await self.viewModel.determineFlow(for: path) + }, label: { + if self.viewModel.loadingPath?.id == path.id { + ProgressView() + } else { + Text(path.title) + } + }) + .restorePurchasesAlert(isPresented: self.$viewModel.showRestoreAlert) + .sheet(item: self.$viewModel.promotionalOfferData, + onDismiss: { + Task { + await self.viewModel.handleSheetDismiss() + } + }, + content: { promotionalOfferData in + PromotionalOfferView(promotionalOffer: promotionalOfferData.promotionalOffer, + product: promotionalOfferData.product, + promoOfferDetails: promotionalOfferData.promoOfferDetails) + }) + .buttonStyle(ManageSubscriptionsButtonStyle()) + .disabled(self.viewModel.loadingPath != nil) + } +} + #if DEBUG @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -268,6 +305,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { refundRequestStatusMessage: "Refund granted successfully!") ManageSubscriptionsView(viewModel: viewModelMonthlyRenewing) .previewDisplayName("Monthly renewing") + .environment(\.localization, CustomerCenterConfigTestData.customerCenterData.localization) let viewModelYearlyExpiring = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, @@ -276,6 +314,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { ManageSubscriptionsView(viewModel: viewModelYearlyExpiring) .previewDisplayName("Yearly expiring") + .environment(\.localization, CustomerCenterConfigTestData.customerCenterData.localization) } } diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift new file mode 100644 index 0000000000..5b91cda35f --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -0,0 +1,182 @@ +// +// 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 +// +// PromotionalOfferView.swift +// +// +// Created by Cesar de la Vega on 17/6/24. +// + +import RevenueCat +import StoreKit +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct PromotionalOfferView: View { + + @StateObject + private var viewModel: PromotionalOfferViewModel + @Environment(\.dismiss) + private var dismiss + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + + init(promotionalOffer: PromotionalOffer, + product: StoreProduct, + promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer) { + let promotionalOfferData = PromotionalOfferData(promotionalOffer: promotionalOffer, + product: product, + promoOfferDetails: promoOfferDetails) + let viewModel = PromotionalOfferViewModel(promotionalOfferData: promotionalOfferData) + self._viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + VStack { + if let details = self.viewModel.promotionalOfferData?.promoOfferDetails, + self.viewModel.error == nil { + Text(details.title) + .font(.title) + .padding() + + Text(details.subtitle) + .font(.title3) + .padding() + + Spacer() + + PromoOfferButtonView(viewModel: viewModel) + + let dismissButtonTitle = self.localization.commonLocalizedString(for: .noThanks) + Button(dismissButtonTitle) { + dismiss() + } + } else { + EmptyView() + .onAppear { + dismiss() + } + } + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct PromoOfferButtonView: View { + + @Environment(\.locale) + private var locale + + @ObservedObject + var viewModel: PromotionalOfferViewModel + + var body: some View { + if let product = self.viewModel.promotionalOfferData?.product, + let discount = self.viewModel.promotionalOfferData?.promotionalOffer.discount { + let mainTitle = discount.localizedPricePerPeriodByPaymentMode(.current) + let localizedProductPricePerPeriod = product.localizedPricePerPeriod(.current) + + Button(action: { + Task { + await viewModel.purchasePromo() + } + }, label: { + VStack { + Text(mainTitle) + .font(.headline) + + let format = Localization.localizedBundle(self.locale) + .localizedString(forKey: "then_price_per_period", value: "then %@", table: nil) + + Text(String(format: format, localizedProductPricePerPeriod)) + .font(.subheadline) + } + }) + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + } + +} + +private extension StoreProductDiscount { + + func localizedPricePerPeriodByPaymentMode(_ locale: Locale) -> String { + let period = self.subscriptionPeriod.periodTitle() + + switch self.paymentMode { + case .freeTrial: + // 3 months for free + return "\(period) for free" + case .payAsYouGo: + // $0.99/month for 3 months + return "\(localizedPricePerPeriod(locale)) for \(localizedNumberOfPeriods())" + case .payUpFront: + // 3 months for $0.99 + return "\(period) for \(self.localizedPriceString)" + } + } + + func localizedNumberOfPeriods() -> String { + let periodString = "\(self.numberOfPeriods) \(self.subscriptionPeriod.durationTitle)" + let pluralized = self.numberOfPeriods > 1 ? periodString + "s" : periodString + return pluralized + } + + func localizedPricePerPeriod(_ locale: Locale) -> String { + let unit = Localization.abbreviatedUnitLocalizedString(for: self.subscriptionPeriod, locale: locale) + return "\(self.localizedPriceString)/\(unit)" + } + +} + +private extension StoreProduct { + + func localizedPricePerPeriod(_ locale: Locale) -> String { + guard let period = self.subscriptionPeriod else { + return self.localizedPriceString + } + + let unit = Localization.abbreviatedUnitLocalizedString(for: period, locale: locale) + return "\(self.localizedPriceString)/\(unit)" + } + +} + +private extension SubscriptionPeriod { + + var durationTitle: String { + switch self.unit { + case .day: return "day" + case .week: return "week" + case .month: return "month" + case .year: return "year" + default: return "Unknown" + } + } + + func periodTitle() -> String { + let periodString = "\(self.value) \(self.durationTitle)" + let pluralized = self.value > 1 ? periodString + "s" : periodString + return pluralized + } + +} + +#endif diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index dc8beb6a04..c236617cf2 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -44,7 +44,11 @@ enum Strings { case executing_restore_logic case executing_external_restore_logic + // Customer Center case could_not_find_subscription_information + case could_not_offer_for_active_subscriptions + case error_fetching_promotional_offer(Error) + case promo_offer_not_loaded } @@ -121,7 +125,16 @@ extension Strings: CustomStringConvertible { "You must have initialized your `PaywallView` appropriately." case .could_not_find_subscription_information: - return "Could not find any active subscription's information" + return "Could not find information for an active subscription" + + case let .error_fetching_promotional_offer(error): + return "Error fetching promotional offer for active product: \(error)" + + case .promo_offer_not_loaded: + return "Promotional offer details not loaded" + + case .could_not_offer_for_active_subscriptions: + return "Could not find offer for any active subscription" } } diff --git a/RevenueCatUI/Resources/en.lproj/Localizable.strings b/RevenueCatUI/Resources/en.lproj/Localizable.strings index 9037347987..2c02fd002b 100644 --- a/RevenueCatUI/Resources/en.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en.lproj/Localizable.strings @@ -28,3 +28,4 @@ "We applied the previously purchased items to your account. Sorry for the inconvenience." = "We applied the previously purchased items to your account. Sorry for the inconvenience."; "Dismiss" = "Dismiss"; "We couldn’t find any additional purchases under this account. \n\nContact support for assistance if you think this is an error." = "We couldn’t find any additional purchases under this account. \n\nContact support for assistance if you think this is an error."; +"then_price_per_period" = "then %@"; diff --git a/RevenueCatUI/Resources/en_AU.lproj/Localizable.strings b/RevenueCatUI/Resources/en_AU.lproj/Localizable.strings index eb59e1d760..285637f33f 100644 --- a/RevenueCatUI/Resources/en_AU.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en_AU.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"then_price_per_period" = "then %@"; diff --git a/RevenueCatUI/Resources/en_CA.lproj/Localizable.strings b/RevenueCatUI/Resources/en_CA.lproj/Localizable.strings index eb59e1d760..285637f33f 100644 --- a/RevenueCatUI/Resources/en_CA.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en_CA.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"then_price_per_period" = "then %@"; diff --git a/RevenueCatUI/Resources/en_GB.lproj/Localizable.strings b/RevenueCatUI/Resources/en_GB.lproj/Localizable.strings index eb59e1d760..285637f33f 100644 --- a/RevenueCatUI/Resources/en_GB.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en_GB.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"then_price_per_period" = "then %@"; diff --git a/RevenueCatUI/Resources/en_US.lproj/Localizable.strings b/RevenueCatUI/Resources/en_US.lproj/Localizable.strings index eb59e1d760..285637f33f 100644 --- a/RevenueCatUI/Resources/en_US.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en_US.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"then_price_per_period" = "then %@"; diff --git a/RevenueCatUI/Resources/es_419.lproj/Localizable.strings b/RevenueCatUI/Resources/es_419.lproj/Localizable.strings index 2c9ca91959..15dba7ae06 100644 --- a/RevenueCatUI/Resources/es_419.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/es_419.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% de descuento"; "Continue" = "Continuar"; "Default_offer_details_with_intro_offer" = "{{ sub_offer_duration }} gratis y luego {{ total_price_and_per_month }}."; +"then_price_per_period" = "y luego %@"; diff --git a/RevenueCatUI/Resources/es_ES.lproj/Localizable.strings b/RevenueCatUI/Resources/es_ES.lproj/Localizable.strings index 2c9ca91959..15dba7ae06 100644 --- a/RevenueCatUI/Resources/es_ES.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/es_ES.lproj/Localizable.strings @@ -17,3 +17,4 @@ "%d%% off" = "%d%% de descuento"; "Continue" = "Continuar"; "Default_offer_details_with_intro_offer" = "{{ sub_offer_duration }} gratis y luego {{ total_price_and_per_month }}."; +"then_price_per_period" = "y luego %@"; diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 6ee8ddd8b0..ac9d889fa8 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -39,6 +39,23 @@ public struct CustomerCenterConfigData { self.localizedStrings = localizedStrings } + public enum CommonLocalizedString: String { + + case noThanks = "no_thanks" + + var defaultValue: String { + switch self { + case .noThanks: + return "No, thanks" + } + } + + } + + public func commonLocalizedString(for key: CommonLocalizedString) -> String { + return self.localizedStrings["common_\(key.rawValue)"] ?? key.defaultValue + } + } public struct HelpPath { @@ -94,10 +111,14 @@ public struct CustomerCenterConfigData { public let iosOfferId: String public let eligible: Bool + public let title: String + public let subtitle: String - public init(iosOfferId: String, eligible: Bool) { + public init(iosOfferId: String, eligible: Bool, title: String, subtitle: String) { self.iosOfferId = iosOfferId self.eligible = eligible + self.title = title + self.subtitle = subtitle } } @@ -116,10 +137,12 @@ public struct CustomerCenterConfigData { public let id: String public let title: String + public let promotionalOffer: PromotionalOffer? - public init(id: String, title: String) { + public init(id: String, title: String, promotionalOffer: PromotionalOffer?) { self.id = id self.title = title + self.promotionalOffer = promotionalOffer } } @@ -290,6 +313,8 @@ extension CustomerCenterConfigData.HelpPath.PromotionalOffer { init(from response: CustomerCenterConfigResponse.HelpPath.PromotionalOffer) { self.iosOfferId = response.iosOfferId self.eligible = response.eligible + self.title = response.title + self.subtitle = response.subtitle } } @@ -308,6 +333,12 @@ extension CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option { init(from response: CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option) { self.id = response.id self.title = response.title + if let promotionalOffer = response.promotionalOffer { + self.promotionalOffer = CustomerCenterConfigData.HelpPath.PromotionalOffer(from: promotionalOffer) + } else { + self.promotionalOffer = nil + } + } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 1241358369..0845a94155 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -59,6 +59,8 @@ struct CustomerCenterConfigResponse { let iosOfferId: String let eligible: Bool + let title: String + let subtitle: String } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift index 22ab3102b6..cc70c923b1 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift @@ -39,8 +39,13 @@ func checkHelpPathDetail(_ detail: CustomerCenterConfigData.HelpPath.PathDetail) func checkPromotionalOffer(_ offer: CustomerCenterConfigData.HelpPath.PromotionalOffer) { let iosOfferId: String = offer.iosOfferId let eligible: Bool = offer.eligible + let title: String = offer.title + let subtitle: String = offer.subtitle - let _: CustomerCenterConfigData.HelpPath.PromotionalOffer = .init(iosOfferId: iosOfferId, eligible: eligible) + let _: CustomerCenterConfigData.HelpPath.PromotionalOffer = .init(iosOfferId: iosOfferId, + eligible: eligible, + title: title, + subtitle: subtitle) } func checkFeedbackSurvey(_ survey: CustomerCenterConfigData.HelpPath.FeedbackSurvey) { @@ -53,8 +58,11 @@ func checkFeedbackSurvey(_ survey: CustomerCenterConfigData.HelpPath.FeedbackSur func checkFeedbackSurveyOption(_ option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) { let id: String = option.id let title: String = option.title + let promotionalOffer: CustomerCenterConfigData.HelpPath.PromotionalOffer? = option.promotionalOffer - let _: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option = .init(id: id, title: title) + let _: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option = .init(id: id, + title: title, + promotionalOffer: promotionalOffer) } func checkScreen(_ screen: CustomerCenterConfigData.Screen) { diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 2c6d391533..360a22f85b 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -16,7 +16,7 @@ // swiftlint:disable file_length type_body_length function_body_length import Nimble -import RevenueCat +@testable import RevenueCat @testable import RevenueCatUI import StoreKit import XCTest @@ -102,11 +102,12 @@ class ManageSubscriptionsViewModelTests: TestCase { ) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil, purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products ), - customerCenterActionHandler: nil) + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) // Act await viewModel.loadScreen() @@ -160,11 +161,12 @@ class ManageSubscriptionsViewModelTests: TestCase { ) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil, purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products ), - customerCenterActionHandler: nil) + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) // Act await viewModel.loadScreen() @@ -224,11 +226,12 @@ class ManageSubscriptionsViewModelTests: TestCase { ) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil, purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products ), - customerCenterActionHandler: nil) + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) // Act await viewModel.loadScreen() @@ -288,11 +291,12 @@ class ManageSubscriptionsViewModelTests: TestCase { ) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil, purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: customerInfo, products: products ), - customerCenterActionHandler: nil) + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) // Act await viewModel.loadScreen() @@ -311,11 +315,11 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testLoadScreenNoActiveSubscription() async { + let mockPurchases = MockManageSubscriptionsPurchases(customerInfo: Fixtures.customerInfoWithoutSubscriptions) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, - purchasesProvider: MockManageSubscriptionsPurchases( - customerInfo: Fixtures.customerInfoWithoutSubscriptions - ), - customerCenterActionHandler: nil) + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases, + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) await viewModel.loadScreen() @@ -324,11 +328,11 @@ class ManageSubscriptionsViewModelTests: TestCase { } func testLoadScreenFailure() async { + let mockPurchases = MockManageSubscriptionsPurchases(customerInfoError: error) let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, - purchasesProvider: MockManageSubscriptionsPurchases( - customerInfoError: error - ), - customerCenterActionHandler: nil) + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases, + loadPromotionalOfferUseCase: MockLoadPromotionalOfferUseCase()) await viewModel.loadScreen() @@ -336,6 +340,100 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(viewModel.state) == .error(error) } + func testLoadsPromotionalOffer() async throws { + let productIdOne = "com.revenuecat.product1" + let productIdTwo = "com.revenuecat.product2" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDateFirst = "2062-04-12T00:03:35Z" + let expirationDateSecond = "2062-05-12T00:03:35Z" + let offerIdentifier = "offer_id" + let product = Fixtures.product(id: productIdOne, + title: "yearly", + duration: .year, + price: 29.99, + offerIdentifier: offerIdentifier) + let products = [ + product, + Fixtures.product(id: productIdTwo, title: "monthly", duration: .month, price: 2.99) + ] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productIdOne, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Subscription( + id: productIdTwo, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled(), + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productIdOne, + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ) + ] + ) + let promoOfferDetails = CustomerCenterConfigData.HelpPath.PromotionalOffer(iosOfferId: offerIdentifier, + eligible: true, + title: "Wait", + subtitle: "Here's an offer for you") + let loadPromotionalOfferUseCase = MockLoadPromotionalOfferUseCase() + loadPromotionalOfferUseCase.mockedProduct = product + loadPromotionalOfferUseCase.mockedPromoOfferDetails = promoOfferDetails + let signedData = PromotionalOffer.SignedData(identifier: "id", + keyIdentifier: "key_i", + nonce: UUID(), + signature: "a signature", + timestamp: 1234) + let discount = MockStoreProductDiscount(offerIdentifier: offerIdentifier, + currencyCode: "usd", + price: 1, + localizedPriceString: "$1.00", + paymentMode: .payAsYouGo, + subscriptionPeriod: SubscriptionPeriod(value: 1, unit: .month), + numberOfPeriods: 1, + type: .introductory) + + loadPromotionalOfferUseCase.mockedPromotionalOffer = PromotionalOffer(discount: discount, + signedData: signedData) + + let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen, + customerCenterActionHandler: nil, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + ), + loadPromotionalOfferUseCase: loadPromotionalOfferUseCase) + + await viewModel.loadScreen() + + let screen = try XCTUnwrap(viewModel.screen) + expect(viewModel.state) == .success + + let pathWithPromotionalOffer = try XCTUnwrap(screen.paths.first { path in + if case .promotionalOffer = path.detail { + return true + } + return false + }) + + expect(loadPromotionalOfferUseCase.offerToLoadPromoFor).to(beNil()) + + await viewModel.determineFlow(for: pathWithPromotionalOffer) + + let loadingPath = try XCTUnwrap(viewModel.loadingPath) + expect(loadingPath.id) == pathWithPromotionalOffer.id + + expect(loadPromotionalOfferUseCase.offerToLoadPromoFor?.iosOfferId) == offerIdentifier + } + private func reformat(ISO8601Date: String) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium @@ -451,7 +549,8 @@ private class Fixtures { title: String, duration: SKProduct.PeriodUnit, price: Decimal, - priceLocale: String = "en_US" + priceLocale: String = "en_US", + offerIdentifier: String? = nil ) -> StoreProduct { // Using SK1 products because they can be mocked, but CustomerCenterViewModel // works with generic `StoreProduct`s regardless of what they contain @@ -459,6 +558,9 @@ private class Fixtures { sk1Product.mockPrice = price sk1Product.mockPriceLocale = Locale(identifier: priceLocale) sk1Product.mockSubscriptionPeriod = SKProductSubscriptionPeriod(numberOfUnits: 1, unit: duration) + if let offerIdentifier = offerIdentifier { + sk1Product.mockDiscount = SKProductDiscount(identifier: offerIdentifier) + } return StoreProduct(sk1Product: sk1Product) } @@ -588,6 +690,7 @@ private extension ManageSubscriptionsViewModelTests { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private class MockSK1Product: SK1Product { + var mockProductIdentifier: String var mockLocalizedTitle: String @@ -650,16 +753,72 @@ private class MockSK1Product: SK1Product { override var subscriptionPeriod: SKProductSubscriptionPeriod? { return mockSubscriptionPeriod } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) fileprivate extension SKProductSubscriptionPeriod { + convenience init(numberOfUnits: Int, unit: SK1Product.PeriodUnit) { self.init() self.setValue(numberOfUnits, forKey: "numberOfUnits") self.setValue(unit.rawValue, forKey: "unit") } + +} + +fileprivate extension SKProductDiscount { + + convenience init(identifier: String) { + self.init() + self.setValue(identifier, forKey: "identifier") + self.setValue(subscriptionPeriod, forKey: "subscriptionPeriod") + } + +} + +private struct MockStoreProductDiscount: StoreProductDiscountType { + + let offerIdentifier: String? + let currencyCode: String? + let price: Decimal + let localizedPriceString: String + let paymentMode: StoreProductDiscount.PaymentMode + let subscriptionPeriod: SubscriptionPeriod + let numberOfPeriods: Int + let type: StoreProductDiscount.DiscountType + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +class MockLoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType { + + var offerToLoadPromoFor: RevenueCat.CustomerCenterConfigData.HelpPath.PromotionalOffer? + + var mockedProduct: StoreProduct? + var mockedPromotionalOffer: PromotionalOffer? + var mockedPromoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer? + + func execute( + promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer + ) async -> Result { + self.offerToLoadPromoFor = promoOfferDetails + if let mockedProduct = mockedProduct, + let mockedPromotionalOffer = mockedPromotionalOffer, + let mockedPromoOfferDetails = mockedPromoOfferDetails { + return .success(PromotionalOfferData(promotionalOffer: mockedPromotionalOffer, + product: mockedProduct, + promoOfferDetails: mockedPromoOfferDetails)) + } else { + return .failure(CustomerCenterError.couldNotFindOfferForActiveProducts) + } + + } + } #endif diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index b3e58d0b94..9ae12554ca 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -47,7 +47,10 @@ class CustomerCenterConfigDataTests: TestCase { id: "path2", title: "Path 2", type: .cancel, - promotionalOffer: .init(iosOfferId: "offer_id", eligible: true), + promotionalOffer: .init(iosOfferId: "offer_id", + eligible: true, + title: "Wait!", + subtitle: "Before you go"), feedbackSurvey: nil ), .init( @@ -60,7 +63,9 @@ class CustomerCenterConfigDataTests: TestCase { .init(id: "id_1", title: "option 1", promotionalOffer: .init(iosOfferId: "offer_id_1", - eligible: true)) + eligible: true, + title: "Wait!", + subtitle: "Before you go")) ]) ) ] diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift index 4d4594133e..f8396c6aff 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -298,7 +298,9 @@ private extension BackendGetCustomerCenterConfigTests { "id": "nwodkdnfaoeb", "promotional_offer": [ "ios_offer_id": "rc-refund-offer", - "eligible": true + "eligible": true, + "title": "Wait!", + "subtitle": "Here's an offer for you" ] as [String: Any], "title": "Request a refund", "type": "REFUND_REQUEST" @@ -315,7 +317,9 @@ private extension BackendGetCustomerCenterConfigTests { "id": "iewrthals", "promotional_offer": [ "ios_offer_id": "rc-cancel-offer", - "eligible": false + "eligible": false, + "title": "Wait!", + "subtitle": "Here's an offer for you" ] as [String: Any], "title": "Too expensive" ] as [String: Any], @@ -323,7 +327,9 @@ private extension BackendGetCustomerCenterConfigTests { "id": "qklpadsfj", "promotional_offer": [ "ios_offer_id": "rc-cancel-offer", - "eligible": false + "eligible": false, + "title": "Wait!", + "subtitle": "Here's an offer for you" ] as [String: Any], "title": "Don't use the app" ] as [String: Any], diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfig.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..13b485ab69 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..58847be783 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..ec56749383 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS13-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..a972b5b8a7 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..40358468e5 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..87a43eff66 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,25 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json new file mode 100644 index 0000000000..2a9720de9c --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json @@ -0,0 +1,44 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "entries" : [ + { + "name" : "customer_info_verification_result", + "properties" : { + "verification_result" : "FAILED" + }, + "timestamp" : "2023-09-06T19:42:08Z", + "version" : 1 + }, + { + "name" : "customer_info_verification_result", + "properties" : { + "verification_result" : "FAILED" + }, + "timestamp" : "2023-09-06T17:45:21Z", + "version" : 1 + } + ] + }, + "method" : "POST", + "url" : "https://api-diagnostics.revenuecat.com/v1/diagnostics" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json new file mode 100644 index 0000000000..1101fbb075 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json @@ -0,0 +1,36 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "2", + "X-StoreKit2-Enabled" : "true", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "entries" : [ + { + "name" : "customer_info_verification_result", + "properties" : { + "verification_result" : "FAILED" + }, + "timestamp" : "2023-09-06T19:42:08Z", + "version" : 1 + } + ] + }, + "method" : "POST", + "url" : "https://api-diagnostics.revenuecat.com/v1/diagnostics" + } +} \ No newline at end of file From 0d7530ecd8d160c47a280ca428f9e23408e4b0d6 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 19 Jul 2024 17:51:39 +0200 Subject: [PATCH 12/90] Move No subscriptions page strings to Localization (#4089) ### Description This is based on the changes in #3968 This PR: - Moves the strings currently hardcode into the `CustomerCenterConfigData.Localization` object. - Fixes an issue compiling paywall tester introduced in a previous PR - Modifies how we read backend strings to not account for the `common_` prefix, which is already removed by the backend. --- .../CustomerCenter/Views/NoSubscriptionsView.swift | 12 +++++++----- .../CustomerCenter/CustomerCenterConfigData.swift | 14 +++++++++++++- .../UI/Views/SamplePaywallsList.swift | 2 -- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 2790e182f5..3d866e489a 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -32,6 +32,9 @@ struct NoSubscriptionsView: View { @Environment(\.dismiss) var dismiss + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + @State private var showRestoreAlert: Bool = false @@ -41,25 +44,24 @@ struct NoSubscriptionsView: View { var body: some View { VStack { - Text("No Subscriptions found") + Text(localization.commonLocalizedString(for: .noSubscriptionsFound)) .font(.title) .padding() - Text("We can try checking your Apple account for any previous purchases") + Text(localization.commonLocalizedString(for: .tryCheckRestore)) .font(.body) .padding() Spacer() - Button("Restore purchases") { + Button(localization.commonLocalizedString(for: .restorePurchases)) { showRestoreAlert = true } .restorePurchasesAlert(isPresented: $showRestoreAlert) .buttonStyle(ManageSubscriptionsButtonStyle()) - Button("Cancel") { + Button(localization.commonLocalizedString(for: .cancel)) { dismiss() } - } } diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index ac9d889fa8..3cf7fafc6b 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -42,18 +42,30 @@ public struct CustomerCenterConfigData { public enum CommonLocalizedString: String { case noThanks = "no_thanks" + case noSubscriptionsFound = "no_subscriptions_found" + case tryCheckRestore = "try_check_restore" + case restorePurchases = "restore_purchases" + case cancel = "cancel" var defaultValue: String { switch self { case .noThanks: return "No, thanks" + case .noSubscriptionsFound: + return "No Subscriptions found" + case .tryCheckRestore: + return "We can try checking your Apple account for any previous purchases" + case .restorePurchases: + return "Restore purchases" + case .cancel: + return "Cancel" } } } public func commonLocalizedString(for key: CommonLocalizedString) -> String { - return self.localizedStrings["common_\(key.rawValue)"] ?? key.defaultValue + return self.localizedStrings[key.rawValue] ?? key.defaultValue } } diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index 71d3fc657b..896ee253b6 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -215,8 +215,6 @@ extension SamplePaywallsList { switch action { case .restoreCompleted(_): print("CustomerCenter: restoreCompleted") - case .purchaseCompleted(_): - print("CustomerCenter: purchaseCompleted") case .restoreStarted: print("CustomerCenter: restoreStarted") case .restoreFailed(_): From b45e274376410397db083d21f0bc914668c892e0 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 19 Jul 2024 18:38:19 +0200 Subject: [PATCH 13/90] [Customer Center] Add colors support (#3983) Builds buttons from the Appearance object in the JSON --- RevenueCat.xcodeproj/project.pbxproj | 12 ++- .../Data/CustomerCenterConfigTestData.swift | 16 ++-- .../Data/CustomerCenterEnvironment.swift | 18 ++++ .../ManageSubscriptionsButtonStyle.swift | 28 ++++++- .../Views/CustomerCenterView.swift | 15 ++-- .../Views/FeedbackSurveyView.swift | 8 +- .../Views/ManageSubscriptionsView.swift | 12 ++- .../Views/NoSubscriptionsView.swift | 5 +- .../Views/PromotionalOfferView.swift | 8 +- .../Views/TintedProgressView.swift | 47 +++++++++++ .../CustomerCenterConfigData.swift | 84 ++++++++++--------- .../CustomerCenterConfigResponse.swift | 10 ++- .../CustomerCenterConfigDataTests.swift | 20 +++-- .../BackendGetCustomerCenterConfigTests.swift | 2 +- 14 files changed, 204 insertions(+), 81 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 7ef206fd12..fba0932d70 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ 351B51C126D450E800BD2BD7 /* OfferingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F575859126C08E3F00C12B97 /* OfferingsManagerTests.swift */; }; 351B51C226D450E800BD2BD7 /* ProductRequestDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E350E57B0A393455A72B40 /* ProductRequestDataTests.swift */; }; 351B51C326D450F200BD2BD7 /* InMemoryCachedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35E3250FBBB03D92E06EC /* InMemoryCachedObjectTests.swift */; }; + 3525D8A42C4AB3D600C21D99 /* CustomerCenterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */; }; 35272E1B26D0029300F22C3B /* DeviceCacheSubscriberAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C4A795A0F056381A1B3 /* DeviceCacheSubscriberAttributesTests.swift */; }; 35272E2226D0048D00F22C3B /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353CBE9CF2572A72A347F /* HTTPClientTests.swift */; }; 352B7D7927BD919B002A47DD /* DangerousSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352B7D7827BD919B002A47DD /* DangerousSettings.swift */; }; @@ -210,7 +211,7 @@ 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */; }; 354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */; }; 354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; }; - 3551E3AB2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */; }; + 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; 3592E8862C2ED51700D7F91D /* CustomerCenterConfigCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */; }; @@ -1173,6 +1174,7 @@ 351B517126D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInMemoryCachedOfferings.swift; sourceTree = ""; }; 351B517326D44F4B00BD2BD7 /* MockPaymentDiscount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaymentDiscount.swift; sourceTree = ""; }; 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRequestFetcher.swift; sourceTree = ""; }; + 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterEnvironment.swift; sourceTree = ""; }; 352B7D7827BD919B002A47DD /* DangerousSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DangerousSettings.swift; sourceTree = ""; }; 3530C18822653E8F00D6DF52 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; 35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsSynchronizer.swift; sourceTree = ""; }; @@ -1198,7 +1200,7 @@ 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromotionalOfferView.swift; sourceTree = ""; }; 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionKey.swift; sourceTree = ""; }; 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = ""; }; - 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterEnvironment.swift; sourceTree = ""; }; + 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintedProgressView.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; 357C9BC022725CFA006BC624 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/iAd.framework; sourceTree = DEVELOPER_DIR; }; @@ -2573,6 +2575,7 @@ 353756562C382C2800A1B8D6 /* Data */ = { isa = PBXGroup; children = ( + 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */, 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */, @@ -2581,7 +2584,6 @@ 35C496052C482ACC0023E924 /* PromotionalOfferData.swift */, 35F249C92C493D970058993A /* LoadPromotionalOfferUseCase.swift */, 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */, - 3551E3AA2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift */, ); path = Data; sourceTree = ""; @@ -2608,6 +2610,7 @@ 3546355E2C391F4D001D7E85 /* PromotionalOfferView.swift */, 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */, + 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */, ); path = Views; sourceTree = ""; @@ -5236,7 +5239,6 @@ 353756682C382C2800A1B8D6 /* CustomerCenterViewModel.swift in Sources */, 887A60BD2C1D037000E1A461 /* TemplateViewType.swift in Sources */, 887A606D2C1D037000E1A461 /* Localization.swift in Sources */, - 3551E3AB2C4A6F1D00D27C25 /* CustomerCenterEnvironment.swift in Sources */, 887A60CA2C1D037000E1A461 /* RemoteImage.swift in Sources */, 887A607B2C1D037000E1A461 /* Bundle+Extensions.swift in Sources */, 353756662C382C2800A1B8D6 /* CustomerCenterError.swift in Sources */, @@ -5267,6 +5269,7 @@ 35F249CC2C493DCC0058993A /* CustomerCenterPurchasesType.swift in Sources */, 887A60672C1D037000E1A461 /* PaywallError.swift in Sources */, 88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */, + 3525D8A42C4AB3D600C21D99 /* CustomerCenterEnvironment.swift in Sources */, 35C496062C482ACC0023E924 /* PromotionalOfferData.swift in Sources */, 887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */, 887A60802C1D037000E1A461 /* Package+VariableDataProvider.swift in Sources */, @@ -5275,6 +5278,7 @@ 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */, + 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */, 887A60BA2C1D037000E1A461 /* Template3View.swift in Sources */, 887A607D2C1D037000E1A461 /* ImageLoader.swift in Sources */, 887A60822C1D037000E1A461 /* PreviewHelpers.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 6be73b9494..38f1770a30 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -91,17 +91,11 @@ enum CustomerCenterConfigTestData { ) ], appearance: .init( - mode: .custom, - light: .init( - accentColor: "#ffffff", - backgroundColor: "#000000", - textColor: "#000000" - ), - dark: .init( - accentColor: "#000000", - backgroundColor: "#ffffff", - textColor: "#ffffff" - ) + // swiftlint:disable force_try + mode: .custom(accentColor: try! .init(light: "#ffffff", dark: "#000000"), + backgroundColor: try! .init(light: "#000000", dark: "#ffffff"), + textColor: try! .init(light: "#000000", dark: "#ffffff")) + // swiftlint:enable force_try ), localization: .init( locale: "en_US", diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift index 7cd36e9a53..ddb823d66c 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -28,6 +28,19 @@ extension CustomerCenterConfigData.Localization { } +struct AppearanceKey: EnvironmentKey { + + static let defaultValue: CustomerCenterConfigData.Appearance = .default + +} + +extension CustomerCenterConfigData.Appearance { + + /// Default ``CustomerCenterConfigData.Appearance`` value for Environment usage + public static let `default` = CustomerCenterConfigData.Appearance(mode: .system) + +} + extension EnvironmentValues { var localization: CustomerCenterConfigData.Localization { @@ -35,4 +48,9 @@ extension EnvironmentValues { set { self[LocalizationKey.self] = newValue } } + var appearance: CustomerCenterConfigData.Appearance { + get { self[AppearanceKey.self] } + set { self[AppearanceKey.self] = newValue } + } + } diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index 0e29eb90a7..a97f68233e 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -14,6 +14,7 @@ // import Foundation +import RevenueCat import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -22,12 +23,15 @@ import SwiftUI @available(watchOS, unavailable) struct ManageSubscriptionsButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { + @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) private var colorScheme + + func makeBody(configuration: ButtonStyleConfiguration) -> some View { configuration.label .padding() .frame(width: 300) - .background(Color.accentColor) - .foregroundColor(.white) + .background(color(from: appearance)) + .foregroundColor(colorScheme == .dark ? Color.black : Color.white) .cornerRadius(10) .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .opacity(configuration.isPressed ? 0.8 : 1.0) @@ -36,6 +40,23 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private extension ManageSubscriptionsButtonStyle { + + func color(from appearance: CustomerCenterConfigData.Appearance) -> Color { + switch appearance.mode { + case .system: + return Color.accentColor + case .custom(accentColor: let accentColor, backgroundColor: _, textColor: _): + return colorScheme == .dark ? accentColor.dark.underlyingColor : accentColor.light.underlyingColor + } + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -45,6 +66,7 @@ struct ManageSubscriptionsButtonStyle_Previews: PreviewProvider { static var previews: some View { Button("Didn't receive purchase") {} .buttonStyle(ManageSubscriptionsButtonStyle()) + .environment(\.appearance, CustomerCenterConfigData.Appearance(mode: .system)) } } diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 6117e01742..007c48eb2f 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -29,30 +29,36 @@ public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel private var localization: CustomerCenterConfigData.Localization + private var appearance: CustomerCenterConfigData.Appearance /// Create a view to handle common customer support tasks public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil, - localization: CustomerCenterConfigData.Localization = .default) { + localization: CustomerCenterConfigData.Localization = .default, + appearance: CustomerCenterConfigData.Appearance = .default) { self._viewModel = .init(wrappedValue: CustomerCenterViewModel(customerCenterActionHandler: customerCenterActionHandler)) self.localization = localization + self.appearance = appearance } fileprivate init(viewModel: CustomerCenterViewModel, - localization: CustomerCenterConfigData.Localization = .default) { + localization: CustomerCenterConfigData.Localization = .default, + appearance: CustomerCenterConfigData.Appearance = .default) { self._viewModel = .init(wrappedValue: viewModel) self.localization = localization + self.appearance = appearance } // swiftlint:disable:next missing_docs public var body: some View { Group { if !self.viewModel.isLoaded { - ProgressView() + TintedProgressView() } else { if let configuration = self.viewModel.configuration { destinationView(configuration: configuration) .environment(\.localization, configuration.localization) + .environment(\.appearance, configuration.appearance) } } } @@ -84,8 +90,7 @@ private extension CustomerCenterView { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { ManageSubscriptionsView(screen: screen, - customerCenterActionHandler: viewModel.customerCenterActionHandler, - localization: configuration.localization) + customerCenterActionHandler: viewModel.customerCenterActionHandler) } else { WrongPlatformView() } diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 5522899d45..40709621d7 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -27,9 +27,10 @@ struct FeedbackSurveyView: View { @StateObject private var viewModel: FeedbackSurveyViewModel - @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance init(feedbackSurveyData: FeedbackSurveyData) { let viewModel = FeedbackSurveyViewModel(feedbackSurveyData: feedbackSurveyData) @@ -69,6 +70,9 @@ struct FeedbackSurveyButtonsView: View { let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] let onOptionSelected: (_ optionSelected: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async -> Void + + @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @Binding var loadingState: String? @@ -79,7 +83,7 @@ struct FeedbackSurveyButtonsView: View { await self.onOptionSelected(option) }, label: { if self.loadingState == option.id { - ProgressView() + TintedProgressView() } else { Text(option.title) } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 12ef3777e7..21141f32b3 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -28,6 +28,9 @@ struct ManageSubscriptionsView: View { @Environment(\.openURL) var openURL + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @StateObject private var viewModel: ManageSubscriptionsViewModel @@ -35,8 +38,7 @@ struct ManageSubscriptionsView: View { private var localization: CustomerCenterConfigData.Localization init(screen: CustomerCenterConfigData.Screen, - customerCenterActionHandler: CustomerCenterActionHandler?, - localization: CustomerCenterConfigData.Localization) { + customerCenterActionHandler: CustomerCenterActionHandler?) { let viewModel = ManageSubscriptionsViewModel(screen: screen, customerCenterActionHandler: customerCenterActionHandler) self._viewModel = .init(wrappedValue: viewModel) @@ -261,12 +263,14 @@ struct ManageSubscriptionButton: View { let path: CustomerCenterConfigData.HelpPath @ObservedObject var viewModel: ManageSubscriptionsViewModel + @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + var body: some View { AsyncButton(action: { await self.viewModel.determineFlow(for: path) }, label: { if self.viewModel.loadingPath?.id == path.id { - ProgressView() + TintedProgressView() } else { Text(path.title) } @@ -306,6 +310,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { ManageSubscriptionsView(viewModel: viewModelMonthlyRenewing) .previewDisplayName("Monthly renewing") .environment(\.localization, CustomerCenterConfigTestData.customerCenterData.localization) + .environment(\.appearance, CustomerCenterConfigTestData.customerCenterData.appearance) let viewModelYearlyExpiring = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData.screens[.management]!, @@ -315,6 +320,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { ManageSubscriptionsView(viewModel: viewModelYearlyExpiring) .previewDisplayName("Yearly expiring") .environment(\.localization, CustomerCenterConfigTestData.customerCenterData.localization) + .environment(\.appearance, CustomerCenterConfigTestData.customerCenterData.appearance) } } diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 3d866e489a..f868a99577 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -44,10 +44,11 @@ struct NoSubscriptionsView: View { var body: some View { VStack { - Text(localization.commonLocalizedString(for: .noSubscriptionsFound)) + Text(self.configuration.screens[.noActive]?.title ?? "No Subscriptions found") .font(.title) .padding() - Text(localization.commonLocalizedString(for: .tryCheckRestore)) + Text(self.configuration.screens[.noActive]?.subtitle ?? + "We can try checking your Apple account for any previous purchases") .font(.body) .padding() diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index 5b91cda35f..b6d2fa97e3 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -26,6 +26,8 @@ import SwiftUI @available(visionOS, unavailable) struct PromotionalOfferView: View { + @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @StateObject private var viewModel: PromotionalOfferViewModel @Environment(\.dismiss) @@ -57,7 +59,7 @@ struct PromotionalOfferView: View { Spacer() - PromoOfferButtonView(viewModel: viewModel) + PromoOfferButtonView(viewModel: self.viewModel, appearance: self.appearance) let dismissButtonTitle = self.localization.commonLocalizedString(for: .noThanks) Button(dismissButtonTitle) { @@ -85,7 +87,9 @@ struct PromoOfferButtonView: View { private var locale @ObservedObject - var viewModel: PromotionalOfferViewModel + private(set) var viewModel: PromotionalOfferViewModel + + private(set) var appearance: CustomerCenterConfigData.Appearance var body: some View { if let product = self.viewModel.promotionalOfferData?.product, diff --git a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift new file mode 100644 index 0000000000..d71026ce6e --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift @@ -0,0 +1,47 @@ +// +// 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 +// +// TintedProgressView.swift +// +// Created by Cesar de la Vega on 19/7/24. + +import Foundation +import RevenueCat +import SwiftUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct TintedProgressView: View { + + @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + ProgressView() + .tint(colorScheme == .dark ? Color.black : Color.white) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct TintedProgressView_Previews: PreviewProvider { + + static var previews: some View { + TintedProgressView() + .environment(\.appearance, CustomerCenterConfigData.Appearance(mode: .system)) + } + +} diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 3cf7fafc6b..91eeac21fd 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -16,6 +16,8 @@ import Foundation // swiftlint:disable missing_docs +public typealias RCColor = PaywallColor + // swiftlint:disable nesting public struct CustomerCenterConfigData { @@ -165,46 +167,33 @@ public struct CustomerCenterConfigData { public struct Appearance { - let mode: AppearanceMode - let light: AppearanceCustomColors - let dark: AppearanceCustomColors + public let mode: AppearanceMode - public init(mode: AppearanceMode, light: AppearanceCustomColors, dark: AppearanceCustomColors) { + public init(mode: AppearanceMode) { self.mode = mode - self.light = light - self.dark = dark } - public struct AppearanceCustomColors { - - let accentColor: String - let backgroundColor: String - let textColor: String + public enum AppearanceMode { - public init(accentColor: String, backgroundColor: String, textColor: String) { - self.accentColor = accentColor - self.backgroundColor = backgroundColor - self.textColor = textColor - } + case system + case custom(accentColor: ColorInformation, + backgroundColor: ColorInformation, + textColor: ColorInformation) } - public enum AppearanceMode: String { + public struct ColorInformation { - case custom = "CUSTOM" - case system = "SYSTEM" + public var light: RCColor + public var dark: RCColor - init(from rawValue: String) { - switch rawValue { - case "CUSTOM": - self = .custom - case "SYSTEM": - self = .system - default: - self = .system - } + public init( + light: String, + dark: String + ) throws { + self.light = try RCColor(stringRepresentation: light) + self.dark = try RCColor(stringRepresentation: dark) } - } } @@ -244,6 +233,7 @@ public struct CustomerCenterConfigData { } +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension CustomerCenterConfigData { init(from response: CustomerCenterConfigResponse) { @@ -270,24 +260,40 @@ extension CustomerCenterConfigData.Screen { } +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension CustomerCenterConfigData.Appearance { init(from response: CustomerCenterConfigResponse.Appearance) { - self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response.mode) - self.light = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.light) - self.dark = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.dark) + self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response) } } -extension CustomerCenterConfigData.Appearance.AppearanceCustomColors { +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension CustomerCenterConfigData.Appearance.AppearanceMode { - init(from response: CustomerCenterConfigResponse.Appearance.AppearanceCustomColors) { - // swiftlint:disable:next todo - // TODO: convert colors to PaywallColor (RCColor) - self.accentColor = response.accentColor - self.backgroundColor = response.backgroundColor - self.textColor = response.textColor + init(from response: CustomerCenterConfigResponse.Appearance) { + switch response.mode { + case .system: + self = .system + case .custom: + do { + let light = response.light + let dark = response.dark + let accent = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.accentColor, + dark: dark.accentColor) + let background = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.backgroundColor, + dark: dark.backgroundColor) + let text = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.textColor, + dark: dark.textColor) + self = .custom(accentColor: accent, + backgroundColor: background, + textColor: text) + } catch { + Logger.error("Failed to parse appearance colors") + self = .system + } + } } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 0845a94155..f946abdf2d 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -83,10 +83,17 @@ struct CustomerCenterConfigResponse { struct Appearance { - let mode: String + let mode: AppearanceMode let light: AppearanceCustomColors let dark: AppearanceCustomColors + enum AppearanceMode: String { + + case system = "SYSTEM" + case custom = "CUSTOM" + + } + struct AppearanceCustomColors { let accentColor: String @@ -131,6 +138,7 @@ extension CustomerCenterConfigResponse.HelpPath.PromotionalOffer: Codable, Equat extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey: Codable, Equatable {} extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance: Codable, Equatable {} +extension CustomerCenterConfigResponse.Appearance.AppearanceMode: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance.AppearanceCustomColors: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen.ScreenType: Codable, Equatable {} diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index 9ae12554ca..c3af7c3bf8 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -26,7 +26,7 @@ class CustomerCenterConfigDataTests: TestCase { let mockResponse = CustomerCenterConfigResponse( customerCenter: .init( appearance: .init( - mode: "CUSTOM", + mode: .custom, light: .init(accentColor: "#FFFFFF", backgroundColor: "#000000", textColor: "#FF0000"), dark: .init(accentColor: "#000000", backgroundColor: "#FFFFFF", textColor: "#00FF00") ), @@ -81,13 +81,17 @@ class CustomerCenterConfigDataTests: TestCase { expect(configData.localization.locale) == "en_US" expect(configData.localization.localizedStrings["key"]) == "value" - expect(configData.appearance.mode.rawValue) == "CUSTOM" - expect(configData.appearance.light.accentColor) == "#FFFFFF" - expect(configData.appearance.light.backgroundColor) == "#000000" - expect(configData.appearance.light.textColor) == "#FF0000" - expect(configData.appearance.dark.accentColor) == "#000000" - expect(configData.appearance.dark.backgroundColor) == "#FFFFFF" - expect(configData.appearance.dark.textColor) == "#00FF00" + switch configData.appearance.mode { + case .system: + fatalError("appearance mode should be custom") + case .custom(accentColor: let accentColor, backgroundColor: let backgroundColor, textColor: let textColor): + expect(accentColor.light.stringRepresentation) == "#FFFFFF" + expect(accentColor.dark.stringRepresentation) == "#000000" + expect(backgroundColor.light.stringRepresentation) == "#000000" + expect(backgroundColor.dark.stringRepresentation) == "#FFFFFF" + expect(textColor.light.stringRepresentation) == "#FF0000" + expect(textColor.dark.stringRepresentation) == "#00FF00" + } expect(configData.screens.count) == 1 let managementScreen = try XCTUnwrap(configData.screens[.management]) diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift index f8396c6aff..e68a58a42f 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -151,7 +151,7 @@ class BackendGetCustomerCenterConfigTests: BaseBackendTests { expect(appearance.light.accentColor) == "#000000" expect(appearance.light.backgroundColor) == "#ffffff" expect(appearance.light.textColor) == "#ffffff" - expect(appearance.mode) == "CUSTOM" + expect(appearance.mode) == .custom let screens = try XCTUnwrap(customerCenter.screens) expect(screens).to(haveCount(2)) From dcbfb7df05bad35df944de53d65556d3b70a1afb Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:38:11 +0200 Subject: [PATCH 14/90] [Customer Center] SubscriptionDetailsView gets its strings from CommonLocalizedString. (#4083) --- .../Data/SubscriptionInformation.swift | 16 ++------- .../Views/ManageSubscriptionsView.swift | 26 +++++++++++--- .../CustomerCenterConfigData.swift | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 8e6a62d984..8188435f02 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -23,20 +23,8 @@ struct SubscriptionInformation { let expirationDateString: String? let productIdentifier: String - var expirationString: String { - return active ? (willRenew ? "Next billing date" : "Expires") : "Expired" - } - - var explanation: String { - return active ? ( - willRenew ? - "This is your subscription with the earliest billing date." : - "This is your subscription with the earliest expiration date." - ) : "This subscription has expired." - } - - private let willRenew: Bool - private let active: Bool + let willRenew: Bool + let active: Bool init(title: String, durationTitle: String, diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 21141f32b3..e685b53eba 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -87,6 +87,7 @@ struct ManageSubscriptionsView: View { if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, + localization: localization, refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) } @@ -147,6 +148,7 @@ struct SubscriptionDetailsView: View { let iconWidth = 22.0 let subscriptionInformation: SubscriptionInformation + let localization: CustomerCenterConfigData.Localization let refundRequestStatusMessage: String? var body: some View { @@ -154,7 +156,14 @@ struct SubscriptionDetailsView: View { VStack(alignment: .leading) { Text("\(subscriptionInformation.title)") .font(.headline) - Text("\(subscriptionInformation.explanation)") + + let explanation = subscriptionInformation.active ? ( + subscriptionInformation.willRenew ? + localization.commonLocalizedString(for: .subEarliestRenewal) : + localization.commonLocalizedString(for: .subEarliestExpiration) + ) : localization.commonLocalizedString(for: .subExpired) + + Text("\(explanation)") .frame(maxWidth: 200, alignment: .leading) .font(.caption) .foregroundColor(Color(UIColor.secondaryLabel)) @@ -165,7 +174,7 @@ struct SubscriptionDetailsView: View { .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text("Billing cycle") + Text(localization.commonLocalizedString(for: .billingCycle)) .font(.caption2) .foregroundColor(Color(UIColor.secondaryLabel)) Text("\(subscriptionInformation.durationTitle)") @@ -178,7 +187,7 @@ struct SubscriptionDetailsView: View { .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text("Current price") + Text(localization.commonLocalizedString(for: .currentPrice)) .font(.caption2) .foregroundColor(Color(UIColor.secondaryLabel)) Text("\(subscriptionInformation.price)") @@ -187,12 +196,19 @@ struct SubscriptionDetailsView: View { } if let nextRenewal = subscriptionInformation.expirationDateString { + + let expirationString = subscriptionInformation.active ? ( + subscriptionInformation.willRenew ? + localization.commonLocalizedString(for: .nextBillingDate) : + localization.commonLocalizedString(for: .expires) + ) : localization.commonLocalizedString(for: .expired) + HStack(alignment: .center) { Image(systemName: "calendar") .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text("\(subscriptionInformation.expirationString)") + Text("\(expirationString)") .font(.caption2) .foregroundColor(Color(UIColor.secondaryLabel)) Text("\(String(describing: nextRenewal))") @@ -207,7 +223,7 @@ struct SubscriptionDetailsView: View { .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text("Refund status") + Text(localization.commonLocalizedString(for: .refundStatus)) .font(.caption2) .foregroundColor(Color(UIColor.secondaryLabel)) Text("\(refundRequestStatusMessage)") diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 91eeac21fd..454c498fee 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -48,6 +48,18 @@ public struct CustomerCenterConfigData { case tryCheckRestore = "try_check_restore" case restorePurchases = "restore_purchases" case cancel = "cancel" + case billingCycle = "billing_cycle" + case currentPrice = "current_price" + case expired = "expired" + case expires = "expires" + case nextBillingDate = "next_billing_date" + case refundCanceled = "refund_canceled" + case refundErrorGeneric = "refund_error_generic" + case refundGranted = "refund_granted" + case refundStatus = "refund_status" + case subEarliestExpiration = "sub_earliest_expiration" + case subEarliestRenewal = "sub_earliest_renewal" + case subExpired = "sub_expired" var defaultValue: String { switch self { @@ -61,6 +73,30 @@ public struct CustomerCenterConfigData { return "Restore purchases" case .cancel: return "Cancel" + case .billingCycle: + return "Billing cycle" + case .currentPrice: + return "Current price" + case .expired: + return "Expired" + case .expires: + return "Expires" + case .nextBillingDate: + return "Next billing date" + case .refundCanceled: + return "Refund canceled" + case .refundErrorGeneric: + return "An error occurred while processing the refund request. Please try again." + case .refundGranted: + return "Refund granted successfully!" + case .refundStatus: + return "Refund status" + case .subEarliestExpiration: + return "This is your subscription with the earliest expiration date." + case .subEarliestRenewal: + return "This is your subscription with the earliest billing date." + case .subExpired: + return "This subscription has expired." } } From dc22e0d13c6785707b5ac9737ec70cde045d5761 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 24 Jul 2024 15:04:52 +0200 Subject: [PATCH 15/90] ignore tags --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eaf49cb7c7..f221c80b16 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,7 +58,9 @@ aliases: release-tags: &release-tags filters: tags: - ignore: /^.*-SNAPSHOT/ + ignore: + - /^.*-SNAPSHOT/ + - /^.*-customercenter.alpha.*/ branches: ignore: /.*/ release-branches-and-main: &release-branches-and-main From acbc39164f25ab0304eac979a94df78295b5a444 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 29 Jul 2024 10:05:46 +0200 Subject: [PATCH 16/90] [Customer Center] Fix project.pbxproj (#4122) I think this comes from a bad merge, because this project doesn't touch that file. Xcode is automatically cleaning it up --- RevenueCat.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index e055921f95..8d92a4f415 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 2C6CC1162B8D2B6900432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6CC1152B8D2B6800432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift */; }; 2C7F0AD32B8EEB4600381179 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F0AD22B8EEB4600381179 /* RateLimiter.swift */; }; 2C7F0AD62B8EEF7B00381179 /* RateLimiterRests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F0AD42B8EEF0B00381179 /* RateLimiterRests.swift */; }; - 2C99EB4A2C3881DA0059619B /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FDA2C1D037000E1A461 /* TestData.swift */; }; 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */; }; 2CD2C544278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2CD2C541278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit */; }; 2CD72942268A823900BFC976 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD72941268A823900BFC976 /* Data+Extensions.swift */; }; @@ -5278,7 +5277,6 @@ 1E5F8F6E2C4515430041EECD /* View+PresentCustomerCenter.swift in Sources */, 887A60C12C1D037000E1A461 /* DebugErrorView.swift in Sources */, 887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */, - 887A60782C1D037000E1A461 /* TestData.swift in Sources */, 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */, 35F249CC2C493DCC0058993A /* CustomerCenterPurchasesType.swift in Sources */, 887A60672C1D037000E1A461 /* PaywallError.swift in Sources */, From 29340cabb227dd35867833009a5d2f7cc5cabe3a Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 29 Jul 2024 12:22:55 +0200 Subject: [PATCH 17/90] [Customer Center] Fix `BackendGetCustomerCenterConfigTests` (#4124) --- .../BackendGetCustomerCenterConfigTests.swift | 2 +- .../iOS16-testGetCustomerCenterConfig.1.json | 1 + ...omerCenterConfigCachesForSameUserID.1.json | 1 + ...CustomerCenterConfigCallsHTTPMethod.1.json | 1 + ...onfigCallsHTTPMethodWithRandomDelay.1.json | 1 + ...GetCustomerCenterConfigFailSendsNil.1.json | 1 + ...rCenterConfigNetworkErrorSendsError.1.json | 1 + ...etCustomerCenterConfigPassesLocales.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.2.json | 1 + ...testRepeatedRequestsLogDebugMessage.1.json | 1 + .../iOS17-testGetCustomerCenterConfig.1.json | 1 + ...omerCenterConfigCachesForSameUserID.1.json | 1 + ...CustomerCenterConfigCallsHTTPMethod.1.json | 1 + ...onfigCallsHTTPMethodWithRandomDelay.1.json | 1 + ...GetCustomerCenterConfigFailSendsNil.1.json | 1 + ...rCenterConfigNetworkErrorSendsError.1.json | 1 + ...etCustomerCenterConfigPassesLocales.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.2.json | 1 + ...testRepeatedRequestsLogDebugMessage.1.json | 1 + .../macOS-testGetCustomerCenterConfig.1.json | 1 + ...omerCenterConfigCachesForSameUserID.1.json | 1 + ...CustomerCenterConfigCallsHTTPMethod.1.json | 1 + ...onfigCallsHTTPMethodWithRandomDelay.1.json | 1 + ...GetCustomerCenterConfigFailSendsNil.1.json | 1 + ...rCenterConfigNetworkErrorSendsError.1.json | 1 + ...etCustomerCenterConfigPassesLocales.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.1.json | 1 + ...rConfigDoesntCacheForMultipleUserID.2.json | 1 + ...testRepeatedRequestsLogDebugMessage.1.json | 1 + ...watchOS-testGetCustomerCenterConfig.1.json | 26 +++++++++++++++++++ ...omerCenterConfigCachesForSameUserID.1.json | 26 +++++++++++++++++++ ...CustomerCenterConfigCallsHTTPMethod.1.json | 26 +++++++++++++++++++ ...onfigCallsHTTPMethodWithRandomDelay.1.json | 26 +++++++++++++++++++ ...GetCustomerCenterConfigFailSendsNil.1.json | 26 +++++++++++++++++++ ...rCenterConfigNetworkErrorSendsError.1.json | 26 +++++++++++++++++++ ...etCustomerCenterConfigPassesLocales.1.json | 26 +++++++++++++++++++ ...rConfigDoesntCacheForMultipleUserID.1.json | 26 +++++++++++++++++++ ...rConfigDoesntCacheForMultipleUserID.2.json | 26 +++++++++++++++++++ ...testRepeatedRequestsLogDebugMessage.1.json | 26 +++++++++++++++++++ ...DiagnosticsEventsWithMultipleEvents.1.json | 1 + ...stPostDiagnosticsEventsWithOneEvent.1.json | 1 + 43 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfig.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCachesForSameUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigFailSendsNil.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigPassesLocales.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testRepeatedRequestsLogDebugMessage.1.json diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift index e68a58a42f..dfd2bcb1bf 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -37,7 +37,7 @@ class BackendGetCustomerCenterConfigTests: BaseBackendTests { expect(result).to(beSuccess()) expect(self.httpClient.calls).to(haveCount(1)) - expect(self.operationDispatcher.invokedDispatchOnWorkerThreadDelayParam) == Delay.none + expect(self.operationDispatcher.invokedDispatchOnWorkerThreadDelayParam) == JitterableDelay.none } func testGetCustomerCenterConfigPassesLocales() { diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfig.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigFailSendsNil.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json index a972b5b8a7..f5d34d983e 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerCenterConfigPassesLocales.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN,es_ES", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json index 40358468e5..f501fcb008 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS16-testRepeatedRequestsLogDebugMessage.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfig.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigFailSendsNil.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json index a972b5b8a7..f5d34d983e 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerCenterConfigPassesLocales.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN,es_ES", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json index 40358468e5..f501fcb008 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json index 87a43eff66..d85422eb46 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/iOS17-testRepeatedRequestsLogDebugMessage.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfig.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigFailSendsNil.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json index 70ecfa1cb5..dfcf03fe1d 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerCenterConfigPassesLocales.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN,es_ES", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json index a902ee3635..b70282085a 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json index ed71050ec3..ad501b29ec 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/macOS-testRepeatedRequestsLogDebugMessage.1.json @@ -11,6 +11,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfig.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfig.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfig.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCachesForSameUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCachesForSameUserID.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCachesForSameUserID.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethod.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigCallsHTTPMethodWithRandomDelay.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigFailSendsNil.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigFailSendsNil.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigFailSendsNil.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigNetworkErrorSendsError.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigPassesLocales.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigPassesLocales.1.json new file mode 100644 index 0000000000..fbb48bd81a --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerCenterConfigPassesLocales.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN,es_ES", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json new file mode 100644 index 0000000000..939bb46273 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testGetCustomerConfigDoesntCacheForMultipleUserID.2.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user_id_2" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testRepeatedRequestsLogDebugMessage.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testRepeatedRequestsLogDebugMessage.1.json new file mode 100644 index 0000000000..55632fbc81 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendGetCustomerCenterConfigTests/watchOS-testRepeatedRequestsLogDebugMessage.1.json @@ -0,0 +1,26 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", + "X-Storefront" : "USA", + "X-StoreKit-Version" : "1", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : null, + "method" : "GET", + "url" : "https://api.revenuecat.com/v1/customercenter/user" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json index 2a9720de9c..4503ac1504 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithMultipleEvents.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json index 1101fbb075..ee64cd2f28 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostDiagnosticsTests/iOS16-testPostDiagnosticsEventsWithOneEvent.1.json @@ -12,6 +12,7 @@ "X-Platform-Flavor" : "native", "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", "X-Preferred-Locales" : "en_EN", + "X-Retry-Count" : "0", "X-Storefront" : "USA", "X-StoreKit-Version" : "2", "X-StoreKit2-Enabled" : "true", From 2977e6b23162d90add25025343c4f8bb6ebce261 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 29 Jul 2024 18:55:43 +0200 Subject: [PATCH 18/90] [Customer Center] Add contact support button (#4023) Add contact support button to the `ManageSubscriptionsView` --------- Co-authored-by: Toni Rico --- .../Data/CustomerCenterConfigTestData.swift | 3 +- .../Data/CustomerCenterEnvironment.swift | 21 +++++++--- .../CustomerCenter/URLUtilities.swift | 26 ++++++++---- .../Views/CustomerCenterView.swift | 2 + .../Views/ManageSubscriptionsView.swift | 25 +++++------ .../Views/RestorePurchasesAlert.swift | 23 +++++++++-- .../CustomerCenterConfigData.swift | 41 +++++++++++++++++-- .../CustomerCenterConfigDataAPI.swift | 6 ++- 8 files changed, 115 insertions(+), 32 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 38f1770a30..fad3573b18 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -103,7 +103,8 @@ enum CustomerCenterConfigTestData { "cancel": "Cancel", "back": "Back" ] - ) + ), + support: .init(email: "test-support@revenuecat.com") ) static let subscriptionInformationMonthlyRenewing: SubscriptionInformation = .init( diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift index ddb823d66c..a2321b6a45 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -21,16 +21,22 @@ struct LocalizationKey: EnvironmentKey { } -extension CustomerCenterConfigData.Localization { +struct AppearanceKey: EnvironmentKey { - /// Default ``CustomerCenterConfigData.Localization`` value for Environment usage - public static let `default` = CustomerCenterConfigData.Localization(locale: "en_US", localizedStrings: [:]) + static let defaultValue: CustomerCenterConfigData.Appearance = .default } -struct AppearanceKey: EnvironmentKey { +struct SupportKey: EnvironmentKey { - static let defaultValue: CustomerCenterConfigData.Appearance = .default + static let defaultValue: CustomerCenterConfigData.Support? = nil + +} + +extension CustomerCenterConfigData.Localization { + + /// Default ``CustomerCenterConfigData.Localization`` value for Environment usage + public static let `default` = CustomerCenterConfigData.Localization(locale: "en_US", localizedStrings: [:]) } @@ -53,4 +59,9 @@ extension EnvironmentValues { set { self[AppearanceKey.self] = newValue } } + var supportInformation: CustomerCenterConfigData.Support? { + get { self[SupportKey.self] } + set { self[SupportKey.self] = newValue } + } + } diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index 2de1de68a6..2f79f0f9ba 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -14,19 +14,31 @@ // import Foundation +import SwiftUI enum URLUtilities { - static func createMailURL() -> URL? { - let subject = "Support Request" - let body = "Please describe your issue or question." +#if os(iOS) + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + static func createMailURLIfPossible(email: String, subject: String, body: String) -> URL? { let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" - // swiftlint:disable:next todo - // TODO: make configurable - let urlString = "mailto:support@revenuecat.com?subject=\(encodedSubject)&body=\(encodedBody)" - return URL(string: urlString) + let urlString = "mailto:\(email)?subject=\(encodedSubject)&body=\(encodedBody)" + + if let url = URL(string: urlString), + UIApplication.shared.canOpenURL(url) { + return url + } + + return nil } +#endif + } diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 007c48eb2f..2f06147ed2 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -30,6 +30,7 @@ public struct CustomerCenterView: View { private var localization: CustomerCenterConfigData.Localization private var appearance: CustomerCenterConfigData.Appearance + private var supportInformation: CustomerCenterConfigData.Support? /// Create a view to handle common customer support tasks public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil, @@ -59,6 +60,7 @@ public struct CustomerCenterView: View { destinationView(configuration: configuration) .environment(\.localization, configuration.localization) .environment(\.appearance, configuration.appearance) + .environment(\.supportInformation, configuration.support) } } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index e685b53eba..ff7102a0c8 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -25,18 +25,14 @@ import SwiftUI @available(visionOS, unavailable) struct ManageSubscriptionsView: View { - @Environment(\.openURL) - var openURL - @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization @StateObject private var viewModel: ManageSubscriptionsViewModel - @Environment(\.localization) - private var localization: CustomerCenterConfigData.Localization - init(screen: CustomerCenterConfigData.Screen, customerCenterActionHandler: CustomerCenterActionHandler?) { let viewModel = ManageSubscriptionsViewModel(screen: screen, @@ -87,7 +83,7 @@ struct ManageSubscriptionsView: View { if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - localization: localization, + localization: self.localization, refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) } @@ -251,15 +247,20 @@ struct ManageSubscriptionsButtonsView: View { var viewModel: ManageSubscriptionsViewModel @Binding var loadingPath: CustomerCenterConfigData.HelpPath? + @Environment(\.openURL) + var openURL + + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization var body: some View { VStack(spacing: 16) { let filteredPaths = self.viewModel.screen.paths.filter { path in - #if targetEnvironment(macCatalyst) - return path.type == .refundRequest - #else - return true - #endif +#if targetEnvironment(macCatalyst) + return path.type == .refundRequest +#else + return true +#endif } ForEach(filteredPaths, id: \.id) { path in ManageSubscriptionButton(path: path, viewModel: self.viewModel) diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 1dea4e9e69..55e319417d 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -37,6 +37,10 @@ struct RestorePurchasesAlert: ViewModifier { private var alertType: AlertType = .restorePurchases @Environment(\.dismiss) private var dismiss + @Environment(\.localization) + private var localization + @Environment(\.supportInformation) + private var supportInformation: CustomerCenterConfigData.Support? enum AlertType: Identifiable { case purchasesRecovered, purchasesNotFound, restorePurchases @@ -60,14 +64,14 @@ struct RestorePurchasesAlert: ViewModifier { self.setAlertType(alertType) } }), - secondaryButton: .cancel(Text("Cancel")) + secondaryButton: .cancel(Text(localization.commonLocalizedString(for: .cancel))) ) case .purchasesRecovered: return Alert(title: Text("Purchases recovered!"), message: Text("We applied the previously purchased items to your account. " + "Sorry for the inconvenience."), - dismissButton: .default(Text("Dismiss")) { + dismissButton: .default(Text(localization.commonLocalizedString(for: .dismiss))) { dismiss() }) @@ -75,7 +79,20 @@ struct RestorePurchasesAlert: ViewModifier { return Alert(title: Text(""), message: Text("We couldn’t find any additional purchases under this account. \n\n" + "Contact support for assistance if you think this is an error."), - dismissButton: .default(Text("Dismiss")) { + primaryButton: .default(Text(localization.commonLocalizedString(for: .contactSupport)), + action: { + let subject = self.localization.commonLocalizedString(for: .defaultSubject) + let body = self.localization.commonLocalizedString(for: .defaultBody) + if let supportInformation = self.supportInformation, + let url = URLUtilities.createMailURLIfPossible(email: supportInformation.email, + subject: subject, + body: body) { + Task { + openURL(url) + } + } + }), + secondaryButton: .default(Text(localization.commonLocalizedString(for: .dismiss))) { dismiss() }) } diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 454c498fee..8cf1ff8838 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -15,20 +15,24 @@ import Foundation -// swiftlint:disable missing_docs +// swiftlint:disable missing_docs nesting file_length public typealias RCColor = PaywallColor -// swiftlint:disable nesting public struct CustomerCenterConfigData { public let screens: [Screen.ScreenType: Screen] public let appearance: Appearance public let localization: Localization + public let support: Support - public init(screens: [Screen.ScreenType: Screen], appearance: Appearance, localization: Localization) { + public init(screens: [Screen.ScreenType: Screen], + appearance: Appearance, + localization: Localization, + support: Support) { self.screens = screens self.appearance = appearance self.localization = localization + self.support = support } public struct Localization { @@ -60,6 +64,10 @@ public struct CustomerCenterConfigData { case subEarliestExpiration = "sub_earliest_expiration" case subEarliestRenewal = "sub_earliest_renewal" case subExpired = "sub_expired" + case contactSupport = "contact_support" + case defaultBody = "default_body" + case defaultSubject = "default_subject" + case dismiss = "dismiss" var defaultValue: String { switch self { @@ -97,6 +105,14 @@ public struct CustomerCenterConfigData { return "This is your subscription with the earliest billing date." case .subExpired: return "This subscription has expired." + case .contactSupport: + return "Contact support" + case .defaultBody: + return "Please describe your issue or question." + case .defaultSubject: + return "Support Request" + case .dismiss: + return "Dismiss" } } @@ -267,6 +283,16 @@ public struct CustomerCenterConfigData { } + public struct Support { + + public let email: String + + public init(email: String) { + self.email = email + } + + } + } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @@ -280,6 +306,7 @@ extension CustomerCenterConfigData { let type = CustomerCenterConfigData.Screen.ScreenType(from: $0.key) return (type, Screen(from: $0.value, localization: localization)) }) + self.support = Support(from: response.customerCenter.support) } } @@ -396,3 +423,11 @@ extension CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option { } } + +extension CustomerCenterConfigData.Support { + + init(from response: CustomerCenterConfigResponse.Support) { + self.email = response.email + } + +} diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift index cc70c923b1..a0a1ba95f1 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift @@ -12,8 +12,12 @@ func checkCustomerCenterConfigData(_ data: CustomerCenterConfigData) { let screens: [CustomerCenterConfigData.Screen.ScreenType: CustomerCenterConfigData.Screen] = data.screens let appearance: CustomerCenterConfigData.Appearance = data.appearance let localization: CustomerCenterConfigData.Localization = data.localization + let support: CustomerCenterConfigData.Support = data.support - let _: CustomerCenterConfigData = .init(screens: screens, appearance: appearance, localization: localization) + let _: CustomerCenterConfigData = .init(screens: screens, + appearance: appearance, + localization: localization, + support: support) } func checkHelpPath(_ path: CustomerCenterConfigData.HelpPath) { From c2551075b03be9b70c4761cb465d621b2989c96b Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 31 Jul 2024 17:14:25 +0200 Subject: [PATCH 19/90] [Customer Center] Fix checking eligibility (#4138) We were not checking if the user is eligible for the promotional offer before trying to load it --- .../ViewModels/FeedbackSurveyViewModel.swift | 3 +- .../ManageSubscriptionsViewModel.swift | 19 +-- .../ManageSubscriptionsViewModelTests.swift | 110 ++++++++++++++++++ 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift index 42b2ddced9..0ba2e52276 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift @@ -50,7 +50,8 @@ class FeedbackSurveyViewModel: ObservableObject { } func handleAction(for option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async { - if let promotionalOffer = option.promotionalOffer { + if let promotionalOffer = option.promotionalOffer, + promotionalOffer.eligible { self.loadingState = option.id let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promotionalOffer) switch result { diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index f88bdc4de5..a8ef1d341c 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -127,15 +127,20 @@ class ManageSubscriptionsViewModel: ObservableObject { } } case let .promotionalOffer(promotionalOffer): - self.loadingPath = path - let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promotionalOffer) - switch result { - case .success(let promotionalOfferData): - self.promotionalOfferData = promotionalOfferData - case .failure: + if promotionalOffer.eligible { + self.loadingPath = path + let result = await loadPromotionalOfferUseCase.execute(promoOfferDetails: promotionalOffer) + switch result { + case .success(let promotionalOfferData): + self.promotionalOfferData = promotionalOfferData + case .failure: + await self.onPathSelected(path: path) + self.loadingPath = nil + } + } else { await self.onPathSelected(path: path) - self.loadingPath = nil } + default: await self.onPathSelected(path: path) } diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 360a22f85b..e7cb1dc9bc 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -434,6 +434,97 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(loadPromotionalOfferUseCase.offerToLoadPromoFor?.iosOfferId) == offerIdentifier } + func testDoesNotLoadPromotionalOfferIfNotEligible() async throws { + let productIdOne = "com.revenuecat.product1" + let productIdTwo = "com.revenuecat.product2" + let purchaseDate = "2022-04-12T00:03:28Z" + let expirationDateFirst = "2062-04-12T00:03:35Z" + let expirationDateSecond = "2062-05-12T00:03:35Z" + let offerIdentifier = "offer_id" + let product = Fixtures.product(id: productIdOne, + title: "yearly", + duration: .year, + price: 29.99, + offerIdentifier: offerIdentifier) + let products = [ + product, + Fixtures.product(id: productIdTwo, title: "monthly", duration: .month, price: 2.99) + ] + let customerInfo = Fixtures.customerInfo( + subscriptions: [ + Fixtures.Subscription( + id: productIdOne, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ), + Fixtures.Subscription( + id: productIdTwo, + store: "app_store", + purchaseDate: purchaseDate, + expirationDate: expirationDateSecond + ) + ].shuffled(), + entitlements: [ + Fixtures.Entitlement( + entitlementId: "premium", + productId: productIdOne, + purchaseDate: purchaseDate, + expirationDate: expirationDateFirst + ) + ] + ) + let promoOfferDetails = CustomerCenterConfigData.HelpPath.PromotionalOffer(iosOfferId: offerIdentifier, + eligible: false, + title: "Wait", + subtitle: "Here's an offer for you") + let loadPromotionalOfferUseCase = MockLoadPromotionalOfferUseCase() + loadPromotionalOfferUseCase.mockedProduct = product + loadPromotionalOfferUseCase.mockedPromoOfferDetails = promoOfferDetails + let signedData = PromotionalOffer.SignedData(identifier: "id", + keyIdentifier: "key_i", + nonce: UUID(), + signature: "a signature", + timestamp: 1234) + let discount = MockStoreProductDiscount(offerIdentifier: offerIdentifier, + currencyCode: "usd", + price: 1, + localizedPriceString: "$1.00", + paymentMode: .payAsYouGo, + subscriptionPeriod: SubscriptionPeriod(value: 1, unit: .month), + numberOfPeriods: 1, + type: .introductory) + + loadPromotionalOfferUseCase.mockedPromotionalOffer = PromotionalOffer(discount: discount, + signedData: signedData) + + let viewModel = ManageSubscriptionsViewModel(screen: Fixtures.screenWithIneligiblePromo, + customerCenterActionHandler: nil, + purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: customerInfo, + products: products + ), + loadPromotionalOfferUseCase: loadPromotionalOfferUseCase) + + await viewModel.loadScreen() + + let screen = try XCTUnwrap(viewModel.screen) + expect(viewModel.state) == .success + + let pathWithPromotionalOffer = try XCTUnwrap(screen.paths.first { path in + if case .promotionalOffer = path.detail { + return true + } + return false + }) + + expect(loadPromotionalOfferUseCase.offerToLoadPromoFor).to(beNil()) + + await viewModel.determineFlow(for: pathWithPromotionalOffer) + + expect(loadPromotionalOfferUseCase.offerToLoadPromoFor).to(beNil()) + } + private func reformat(ISO8601Date: String) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium @@ -678,6 +769,25 @@ private class Fixtures { ) }() + static let screenWithIneligiblePromo: CustomerCenterConfigData.Screen = .init( + type: .management, + title: "Manage Subscription", + subtitle: "Manage your subscription details here", + paths: [ + .init( + id: "1", + title: "Didn't receive purchase", + type: .missingPurchase, + detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer( + iosOfferId: "offer_id", + eligible: false, + title: "title", + subtitle: "subtitle" + )) + ) + ] + ) + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) From 167a609bce99aea248480d80c5927aa64702bcdb Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 1 Aug 2024 15:21:38 +0200 Subject: [PATCH 20/90] [Customer Center] Make colors nullable (#4134) - Adapt changes from https://github.com/RevenueCat/khepri/pull/9707/ which makes colors nullable and adds new colors for buttons - Change background and text color in all screens - Modified buttons to use `.buttonStyle(.borderedProminent)` so they look native by default - Applied accent color to back button in navigation view --- RevenueCat.xcodeproj/project.pbxproj | 4 + .../CustomerCenter/ColorFromAppearance.swift | 22 ++++ .../Data/CustomerCenterConfigTestData.swift | 16 +-- .../Data/CustomerCenterEnvironment.swift | 8 +- .../ManageSubscriptionsButtonStyle.swift | 46 +++----- .../ViewModels/FeedbackSurveyViewModel.swift | 1 + .../Views/FeedbackSurveyView.swift | 42 +++++--- .../Views/ManageSubscriptionsView.swift | 52 ++++++--- .../Views/NoSubscriptionsView.swift | 42 +++++--- .../Views/PromotionalOfferView.swift | 72 +++++++++---- .../Views/RestorePurchasesAlert.swift | 4 +- .../Views/TintedProgressView.swift | 13 ++- .../Views/WrongPlatformView.swift | 43 +++++--- .../CustomerCenterConfigData.swift | 100 +++++++++--------- .../CustomerCenterConfigResponse.swift | 17 +-- .../CustomerCenterConfigDataTests.swift | 34 +++--- .../BackendGetCustomerCenterConfigTests.swift | 1 - 17 files changed, 315 insertions(+), 202 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/ColorFromAppearance.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 549efbe436..0e025aebba 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -214,6 +214,7 @@ 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; + 357CEC702C5940CE00A80837 /* ColorFromAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357CEC6F2C5940CE00A80837 /* ColorFromAppearance.swift */; }; 3592E8862C2ED51700D7F91D /* CustomerCenterConfigCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */; }; 3592E88A2C2ED54A00D7F91D /* CustomerCenterConfigData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */; }; 3592E88C2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3592E88B2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift */; }; @@ -1345,6 +1346,7 @@ 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; 357C9BC022725CFA006BC624 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/iAd.framework; sourceTree = DEVELOPER_DIR; }; + 357CEC6F2C5940CE00A80837 /* ColorFromAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorFromAppearance.swift; sourceTree = ""; }; 3592E8852C2ED51700D7F91D /* CustomerCenterConfigCallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigCallback.swift; sourceTree = ""; }; 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigData.swift; sourceTree = ""; }; 3592E88B2C2ED58900D7F91D /* GetCustomerCenterConfigOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GetCustomerCenterConfigOperation.swift; path = Sources/Networking/Operations/GetCustomerCenterConfigOperation.swift; sourceTree = SOURCE_ROOT; }; @@ -3132,6 +3134,7 @@ 353756632C382C2800A1B8D6 /* URLUtilities.swift */, 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */, 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */, + 357CEC6F2C5940CE00A80837 /* ColorFromAppearance.swift */, ); path = CustomerCenter; sourceTree = ""; @@ -5822,6 +5825,7 @@ 887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */, 887A60802C1D037000E1A461 /* Package+VariableDataProvider.swift in Sources */, 887A60CE2C1D037000E1A461 /* View+PresentPaywall.swift in Sources */, + 357CEC702C5940CE00A80837 /* ColorFromAppearance.swift in Sources */, 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift b/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift new file mode 100644 index 0000000000..52f0b17228 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift @@ -0,0 +1,22 @@ +// +// 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 +// +// View+Appearance.swift +// +// Created by Cesar de la Vega on 30/7/24. + +import Foundation +import RevenueCat +import SwiftUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +func color(from colorInformation: CustomerCenterConfigData.Appearance.ColorInformation, + for colorScheme: ColorScheme) -> Color? { + return colorScheme == .dark ? colorInformation.dark?.underlyingColor : colorInformation.light?.underlyingColor +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index fad3573b18..fb1c5f30ce 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -90,13 +90,7 @@ enum CustomerCenterConfigTestData { ] ) ], - appearance: .init( - // swiftlint:disable force_try - mode: .custom(accentColor: try! .init(light: "#ffffff", dark: "#000000"), - backgroundColor: try! .init(light: "#000000", dark: "#ffffff"), - textColor: try! .init(light: "#000000", dark: "#ffffff")) - // swiftlint:enable force_try - ), + appearance: standardAppearance, localization: .init( locale: "en_US", localizedStrings: [ @@ -107,6 +101,14 @@ enum CustomerCenterConfigTestData { support: .init(email: "test-support@revenuecat.com") ) + static let standardAppearance = CustomerCenterConfigData.Appearance( + accentColor: .init(light: "#ffffff", dark: "#000000"), + textColor: .init(light: "#000000", dark: "#ffffff"), + backgroundColor: .init(light: "#000000", dark: "#ffffff"), + buttonTextColor: .init(light: "#000000", dark: "#ffffff"), + buttonBackgroundColor: .init(light: "#000000", dark: "#ffffff") + ) + static let subscriptionInformationMonthlyRenewing: SubscriptionInformation = .init( title: "Basic", durationTitle: "Monthly", diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift index a2321b6a45..ac4b325dfb 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -43,7 +43,13 @@ extension CustomerCenterConfigData.Localization { extension CustomerCenterConfigData.Appearance { /// Default ``CustomerCenterConfigData.Appearance`` value for Environment usage - public static let `default` = CustomerCenterConfigData.Appearance(mode: .system) + public static let `default` = CustomerCenterConfigData.Appearance( + accentColor: .init(), + textColor: .init(), + backgroundColor: .init(), + buttonTextColor: .init(), + buttonBackgroundColor: .init() + ) } diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index a97f68233e..d77fe71602 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -17,42 +17,28 @@ import Foundation import RevenueCat import SwiftUI +#if os(iOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct ManageSubscriptionsButtonStyle: ButtonStyle { +struct ManageSubscriptionsButtonStyle: PrimitiveButtonStyle { @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance @Environment(\.colorScheme) private var colorScheme - func makeBody(configuration: ButtonStyleConfiguration) -> some View { - configuration.label - .padding() - .frame(width: 300) - .background(color(from: appearance)) - .foregroundColor(colorScheme == .dark ? Color.black : Color.white) - .cornerRadius(10) - .scaleEffect(configuration.isPressed ? 0.95 : 1.0) - .opacity(configuration.isPressed ? 0.8 : 1.0) - .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@available(macOS, unavailable) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -private extension ManageSubscriptionsButtonStyle { - - func color(from appearance: CustomerCenterConfigData.Appearance) -> Color { - switch appearance.mode { - case .system: - return Color.accentColor - case .custom(accentColor: let accentColor, backgroundColor: _, textColor: _): - return colorScheme == .dark ? accentColor.dark.underlyingColor : accentColor.light.underlyingColor - } + func makeBody(configuration: PrimitiveButtonStyleConfiguration) -> some View { + let background = color(from: appearance.buttonBackgroundColor, for: colorScheme) + let textColor = color(from: appearance.buttonTextColor, for: colorScheme) + + Button(action: { configuration.trigger() }, label: { + configuration.label.frame(width: 300) + }) + .buttonStyle(.borderedProminent) + .controlSize(.large) + .applyIf(background != nil, apply: { $0.tint(background) }) + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } } @@ -66,7 +52,9 @@ struct ManageSubscriptionsButtonStyle_Previews: PreviewProvider { static var previews: some View { Button("Didn't receive purchase") {} .buttonStyle(ManageSubscriptionsButtonStyle()) - .environment(\.appearance, CustomerCenterConfigData.Appearance(mode: .system)) + .environment(\.appearance, CustomerCenterConfigTestData.standardAppearance) } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift index 0ba2e52276..64ca0878a8 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift @@ -59,6 +59,7 @@ class FeedbackSurveyViewModel: ObservableObject { self.promotionalOfferData = promotionalOfferData case .failure: self.feedbackSurveyData.onOptionSelected() + self.loadingState = nil } } else { self.feedbackSurveyData.onOptionSelected() diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 40709621d7..b7b98abdf4 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -31,6 +31,8 @@ struct FeedbackSurveyView: View { private var localization: CustomerCenterConfigData.Localization @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme init(feedbackSurveyData: FeedbackSurveyData) { let viewModel = FeedbackSurveyViewModel(feedbackSurveyData: feedbackSurveyData) @@ -38,25 +40,33 @@ struct FeedbackSurveyView: View { } var body: some View { - VStack { - Text(self.viewModel.feedbackSurveyData.configuration.title) - .font(.title) - .padding() + ZStack { + if let background = color(from: appearance.backgroundColor, for: colorScheme) { + background.edgesIgnoringSafeArea(.all) + } + let textColor = color(from: appearance.textColor, for: colorScheme) + + VStack { + Text(self.viewModel.feedbackSurveyData.configuration.title) + .font(.title) + .padding() + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) - Spacer() + Spacer() - FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options, - onOptionSelected: self.viewModel.handleAction(for:), - loadingState: self.$viewModel.loadingState) + FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options, + onOptionSelected: self.viewModel.handleAction(for:), + loadingState: self.$viewModel.loadingState) + } + .sheet( + item: self.$viewModel.promotionalOfferData, + onDismiss: { self.viewModel.handleSheetDismiss() }, + content: { promotionalOfferData in + PromotionalOfferView(promotionalOffer: promotionalOfferData.promotionalOffer, + product: promotionalOfferData.product, + promoOfferDetails: promotionalOfferData.promoOfferDetails) + }) } - .sheet( - item: self.$viewModel.promotionalOfferData, - onDismiss: { self.viewModel.handleSheetDismiss() }, - content: { promotionalOfferData in - PromotionalOfferView(promotionalOffer: promotionalOfferData.promotionalOffer, - product: promotionalOfferData.product, - promoOfferDetails: promotionalOfferData.promoOfferDetails) - }) } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index ff7102a0c8..c7c0948c95 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -29,6 +29,8 @@ struct ManageSubscriptionsView: View { private var appearance: CustomerCenterConfigData.Appearance @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization + @Environment(\.colorScheme) + private var colorScheme @StateObject private var viewModel: ManageSubscriptionsViewModel @@ -45,6 +47,8 @@ struct ManageSubscriptionsView: View { } var body: some View { + let accentColor = color(from: self.appearance.accentColor, for: self.colorScheme) + if #available(iOS 16.0, *) { NavigationStack { content @@ -56,7 +60,7 @@ struct ManageSubscriptionsView: View { } } } - } + }.applyIf(accentColor != nil, apply: { $0.tint(accentColor) }) } else { NavigationView { content @@ -71,29 +75,35 @@ struct ManageSubscriptionsView: View { ) { EmptyView() }) - } + }.applyIf(accentColor != nil, apply: { $0.tint(accentColor) }) } } @ViewBuilder var content: some View { - VStack { - if self.viewModel.isLoaded { - HeaderView(viewModel: self.viewModel) - - if let subscriptionInformation = self.viewModel.subscriptionInformation { - SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - localization: self.localization, - refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) - } + ZStack { + if let background = color(from: appearance.backgroundColor, for: colorScheme) { + background.edgesIgnoringSafeArea(.all) + } - Spacer() + VStack { + if self.viewModel.isLoaded { + HeaderView(viewModel: self.viewModel) - ManageSubscriptionsButtonsView(viewModel: self.viewModel, - loadingPath: self.$viewModel.loadingPath) - } else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) + if let subscriptionInformation = self.viewModel.subscriptionInformation { + SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, + localization: self.localization, + refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) + } + + Spacer() + + ManageSubscriptionsButtonsView(viewModel: self.viewModel, + loadingPath: self.$viewModel.loadingPath) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } } } .task { @@ -127,11 +137,18 @@ struct HeaderView: View { @ObservedObject private(set) var viewModel: ManageSubscriptionsViewModel + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme var body: some View { + let textColor = color(from: appearance.textColor, for: colorScheme) + Text(self.viewModel.screen.title) .font(.title) .padding() + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } } @@ -140,6 +157,7 @@ struct HeaderView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct SubscriptionDetailsView: View { let iconWidth = 22.0 diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index f868a99577..e1f919bc1d 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -34,7 +34,10 @@ struct NoSubscriptionsView: View { @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization - + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme @State private var showRestoreAlert: Bool = false @@ -43,26 +46,35 @@ struct NoSubscriptionsView: View { } var body: some View { - VStack { - Text(self.configuration.screens[.noActive]?.title ?? "No Subscriptions found") - .font(.title) - .padding() - Text(self.configuration.screens[.noActive]?.subtitle ?? - "We can try checking your Apple account for any previous purchases") + let background = color(from: appearance.backgroundColor, for: colorScheme) + let textColor = color(from: appearance.textColor, for: colorScheme) + + ZStack { + if background != nil { + background.edgesIgnoringSafeArea(.all) + } + VStack { + Text(self.configuration.screens[.noActive]?.title ?? "No Subscriptions found") + .font(.title) + .padding() + Text(self.configuration.screens[.noActive]?.subtitle ?? + "We can try checking your Apple account for any previous purchases") .font(.body) .padding() - Spacer() + Spacer() - Button(localization.commonLocalizedString(for: .restorePurchases)) { - showRestoreAlert = true - } - .restorePurchasesAlert(isPresented: $showRestoreAlert) - .buttonStyle(ManageSubscriptionsButtonStyle()) + Button(localization.commonLocalizedString(for: .restorePurchases)) { + showRestoreAlert = true + } + .restorePurchasesAlert(isPresented: $showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) - Button(localization.commonLocalizedString(for: .cancel)) { - dismiss() + Button(localization.commonLocalizedString(for: .cancel)) { + dismiss() + } } + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } } diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index b6d2fa97e3..2db2ac9062 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -26,14 +26,16 @@ import SwiftUI @available(visionOS, unavailable) struct PromotionalOfferView: View { - @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance - @StateObject private var viewModel: PromotionalOfferViewModel @Environment(\.dismiss) private var dismiss @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme init(promotionalOffer: PromotionalOffer, product: StoreProduct, @@ -46,36 +48,66 @@ struct PromotionalOfferView: View { } var body: some View { - VStack { - if let details = self.viewModel.promotionalOfferData?.promoOfferDetails, - self.viewModel.error == nil { - Text(details.title) - .font(.title) - .padding() + ZStack { + if let background = color(from: appearance.backgroundColor, for: colorScheme) { + background.edgesIgnoringSafeArea(.all) + } - Text(details.subtitle) - .font(.title3) - .padding() + VStack { + if self.viewModel.error == nil { + PromotionalOfferHeaderView(viewModel: self.viewModel) - Spacer() + Spacer() - PromoOfferButtonView(viewModel: self.viewModel, appearance: self.appearance) + PromoOfferButtonView(viewModel: self.viewModel, appearance: self.appearance) - let dismissButtonTitle = self.localization.commonLocalizedString(for: .noThanks) - Button(dismissButtonTitle) { - dismiss() - } - } else { - EmptyView() - .onAppear { + let dismissButtonTitle = self.localization.commonLocalizedString(for: .noThanks) + Button(dismissButtonTitle) { dismiss() } + } else { + EmptyView() + .onAppear { + dismiss() + } + } } } } } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct PromotionalOfferHeaderView: View { + + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme + @ObservedObject + private(set) var viewModel: PromotionalOfferViewModel + + var body: some View { + let textColor = color(from: appearance.textColor, for: colorScheme) + if let details = self.viewModel.promotionalOfferData?.promoOfferDetails { + VStack { + Text(details.title) + .font(.title) + .padding() + + Text(details.subtitle) + .font(.title3) + .padding() + }.applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) + } + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 55e319417d..d42004d1ab 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -71,7 +71,7 @@ struct RestorePurchasesAlert: ViewModifier { return Alert(title: Text("Purchases recovered!"), message: Text("We applied the previously purchased items to your account. " + "Sorry for the inconvenience."), - dismissButton: .default(Text(localization.commonLocalizedString(for: .dismiss))) { + dismissButton: .cancel(Text(localization.commonLocalizedString(for: .dismiss))) { dismiss() }) @@ -92,7 +92,7 @@ struct RestorePurchasesAlert: ViewModifier { } } }), - secondaryButton: .default(Text(localization.commonLocalizedString(for: .dismiss))) { + secondaryButton: .cancel(Text(localization.commonLocalizedString(for: .dismiss))) { dismiss() }) } diff --git a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift index d71026ce6e..1618afef49 100644 --- a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift +++ b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift @@ -15,6 +15,8 @@ import Foundation import RevenueCat import SwiftUI +#if os(iOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -27,6 +29,7 @@ struct TintedProgressView: View { var body: some View { ProgressView() + .controlSize(.regular) .tint(colorScheme == .dark ? Color.black : Color.white) } @@ -41,7 +44,15 @@ struct TintedProgressView_Previews: PreviewProvider { static var previews: some View { TintedProgressView() - .environment(\.appearance, CustomerCenterConfigData.Appearance(mode: .system)) + .environment(\.appearance, CustomerCenterConfigData.Appearance( + accentColor: .init(light: "#ffffff", dark: "#000000"), + textColor: .init(light: "#000000", dark: "#ffffff"), + backgroundColor: .init(light: "#000000", dark: "#ffffff"), + buttonTextColor: .init(light: "#000000", dark: "#ffffff"), + buttonBackgroundColor: .init(light: "#000000", dark: "#ffffff") + )) } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 1f1fe1ef77..8af5eb27d8 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -29,8 +29,10 @@ struct WrongPlatformView: View { @State private var store: Store? - @Environment(\.openURL) - private var openURL + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme init() { } @@ -40,23 +42,30 @@ struct WrongPlatformView: View { } var body: some View { - VStack { - - switch store { - case .appStore, .macAppStore, .playStore, .amazon: - let platformName = humanReadablePlatformName(store: store!) - - Text("Your subscription is a \(platformName) subscription.") - .font(.title) - .padding() - Text("Go the app settings on \(platformName) to manage your subscription and billing.") - .padding() - default: - Text("Please contact support to manage your subscription") - .font(.title) - .padding() + ZStack { + if let background = color(from: appearance.backgroundColor, for: colorScheme) { + background.edgesIgnoringSafeArea(.all) } + let textColor = color(from: appearance.textColor, for: colorScheme) + + VStack { + switch store { + case .appStore, .macAppStore, .playStore, .amazon: + let platformName = humanReadablePlatformName(store: store!) + + Text("Your subscription is a \(platformName) subscription.") + .font(.title) + .padding() + Text("Go the app settings on \(platformName) to manage your subscription and billing.") + .padding() + default: + Text("Please contact support to manage your subscription") + .font(.title) + .padding() + } + } + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } .task { if store == nil { diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 8cf1ff8838..feb4490169 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -219,32 +219,52 @@ public struct CustomerCenterConfigData { public struct Appearance { - public let mode: AppearanceMode - - public init(mode: AppearanceMode) { - self.mode = mode - } - - public enum AppearanceMode { - - case system - case custom(accentColor: ColorInformation, - backgroundColor: ColorInformation, - textColor: ColorInformation) - + public let accentColor: ColorInformation + public let textColor: ColorInformation + public let backgroundColor: ColorInformation + public let buttonTextColor: ColorInformation + public let buttonBackgroundColor: ColorInformation + + public init(accentColor: ColorInformation, + textColor: ColorInformation, + backgroundColor: ColorInformation, + buttonTextColor: ColorInformation, + buttonBackgroundColor: ColorInformation) { + self.accentColor = accentColor + self.textColor = textColor + self.backgroundColor = backgroundColor + self.buttonTextColor = buttonTextColor + self.buttonBackgroundColor = buttonBackgroundColor } public struct ColorInformation { - public var light: RCColor - public var dark: RCColor + public var light: RCColor? + public var dark: RCColor? + + public init() { + self.light = nil + self.dark = nil + } public init( - light: String, - dark: String - ) throws { - self.light = try RCColor(stringRepresentation: light) - self.dark = try RCColor(stringRepresentation: dark) + light: String?, + dark: String? + ) { + if let light = light { + do { + self.light = try RCColor(stringRepresentation: light) + } catch { + Logger.error("Failed to parse light color \(light)") + } + } + if let dark = dark { + do { + self.dark = try RCColor(stringRepresentation: dark) + } catch { + Logger.error("Failed to parse dark color \(dark)") + } + } } } @@ -327,36 +347,16 @@ extension CustomerCenterConfigData.Screen { extension CustomerCenterConfigData.Appearance { init(from response: CustomerCenterConfigResponse.Appearance) { - self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response) - } - -} - -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) -extension CustomerCenterConfigData.Appearance.AppearanceMode { - - init(from response: CustomerCenterConfigResponse.Appearance) { - switch response.mode { - case .system: - self = .system - case .custom: - do { - let light = response.light - let dark = response.dark - let accent = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.accentColor, - dark: dark.accentColor) - let background = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.backgroundColor, - dark: dark.backgroundColor) - let text = try CustomerCenterConfigData.Appearance.ColorInformation(light: light.textColor, - dark: dark.textColor) - self = .custom(accentColor: accent, - backgroundColor: background, - textColor: text) - } catch { - Logger.error("Failed to parse appearance colors") - self = .system - } - } + self.accentColor = .init(light: response.light.accentColor, + dark: response.dark.accentColor) + self.textColor = .init(light: response.light.textColor, + dark: response.dark.textColor) + self.backgroundColor = .init(light: response.light.backgroundColor, + dark: response.dark.backgroundColor) + self.buttonTextColor = .init(light: response.light.buttonTextColor, + dark: response.dark.buttonTextColor) + self.buttonBackgroundColor = .init(light: response.light.buttonBackgroundColor, + dark: response.dark.buttonBackgroundColor) } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index f946abdf2d..ad9c362534 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -83,22 +83,16 @@ struct CustomerCenterConfigResponse { struct Appearance { - let mode: AppearanceMode let light: AppearanceCustomColors let dark: AppearanceCustomColors - enum AppearanceMode: String { - - case system = "SYSTEM" - case custom = "CUSTOM" - - } - struct AppearanceCustomColors { - let accentColor: String - let backgroundColor: String - let textColor: String + let accentColor: String? + let textColor: String? + let backgroundColor: String? + let buttonTextColor: String? + let buttonBackgroundColor: String? } @@ -138,7 +132,6 @@ extension CustomerCenterConfigResponse.HelpPath.PromotionalOffer: Codable, Equat extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey: Codable, Equatable {} extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance: Codable, Equatable {} -extension CustomerCenterConfigResponse.Appearance.AppearanceMode: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance.AppearanceCustomColors: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen.ScreenType: Codable, Equatable {} diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index c3af7c3bf8..e8dafc45d6 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -26,9 +26,16 @@ class CustomerCenterConfigDataTests: TestCase { let mockResponse = CustomerCenterConfigResponse( customerCenter: .init( appearance: .init( - mode: .custom, - light: .init(accentColor: "#FFFFFF", backgroundColor: "#000000", textColor: "#FF0000"), - dark: .init(accentColor: "#000000", backgroundColor: "#FFFFFF", textColor: "#00FF00") + light: .init(accentColor: "#A3F9B5", + textColor: "#7B2D26", + backgroundColor: "#E1C6FF", + buttonTextColor: "#0D3F4F", + buttonBackgroundColor: "#FFA07A"), + dark: .init(accentColor: "#5D3FD3", + textColor: "#98FB98", + backgroundColor: "#2F4F4F", + buttonTextColor: "#FFD700", + buttonBackgroundColor: "#8B4513") ), screens: [ "MANAGEMENT": .init( @@ -81,17 +88,16 @@ class CustomerCenterConfigDataTests: TestCase { expect(configData.localization.locale) == "en_US" expect(configData.localization.localizedStrings["key"]) == "value" - switch configData.appearance.mode { - case .system: - fatalError("appearance mode should be custom") - case .custom(accentColor: let accentColor, backgroundColor: let backgroundColor, textColor: let textColor): - expect(accentColor.light.stringRepresentation) == "#FFFFFF" - expect(accentColor.dark.stringRepresentation) == "#000000" - expect(backgroundColor.light.stringRepresentation) == "#000000" - expect(backgroundColor.dark.stringRepresentation) == "#FFFFFF" - expect(textColor.light.stringRepresentation) == "#FF0000" - expect(textColor.dark.stringRepresentation) == "#00FF00" - } + expect(configData.appearance.accentColor.light!.stringRepresentation) == "#A3F9B5" + expect(configData.appearance.accentColor.dark!.stringRepresentation) == "#5D3FD3" + expect(configData.appearance.backgroundColor.light!.stringRepresentation) == "#E1C6FF" + expect(configData.appearance.backgroundColor.dark!.stringRepresentation) == "#2F4F4F" + expect(configData.appearance.textColor.light!.stringRepresentation) == "#7B2D26" + expect(configData.appearance.textColor.dark!.stringRepresentation) == "#98FB98" + expect(configData.appearance.buttonTextColor.light!.stringRepresentation) == "#0D3F4F" + expect(configData.appearance.buttonTextColor.dark!.stringRepresentation) == "#FFD700" + expect(configData.appearance.buttonBackgroundColor.light!.stringRepresentation) == "#FFA07A" + expect(configData.appearance.buttonBackgroundColor.dark!.stringRepresentation) == "#8B4513" expect(configData.screens.count) == 1 let managementScreen = try XCTUnwrap(configData.screens[.management]) diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift index dfd2bcb1bf..e55a52ef73 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -151,7 +151,6 @@ class BackendGetCustomerCenterConfigTests: BaseBackendTests { expect(appearance.light.accentColor) == "#000000" expect(appearance.light.backgroundColor) == "#ffffff" expect(appearance.light.textColor) == "#ffffff" - expect(appearance.mode) == .custom let screens = try XCTUnwrap(customerCenter.screens) expect(screens).to(haveCount(2)) From dbd43aabe4f2baee01daeebf093908635d0a718f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 2 Aug 2024 15:22:32 +0200 Subject: [PATCH 21/90] [Customer Center] Fix for disabled promo offer button (#4142) I noticed the purchase button for the promo offers is always disabled. Changing the order of the modifiers fixes it --- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index c7c0948c95..8643289ecb 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -310,6 +310,8 @@ struct ManageSubscriptionButton: View { Text(path.title) } }) + .buttonStyle(ManageSubscriptionsButtonStyle()) + .disabled(self.viewModel.loadingPath != nil) .restorePurchasesAlert(isPresented: self.$viewModel.showRestoreAlert) .sheet(item: self.$viewModel.promotionalOfferData, onDismiss: { @@ -322,8 +324,6 @@ struct ManageSubscriptionButton: View { product: promotionalOfferData.product, promoOfferDetails: promotionalOfferData.promoOfferDetails) }) - .buttonStyle(ManageSubscriptionsButtonStyle()) - .disabled(self.viewModel.loadingPath != nil) } } From 6084a2c170f05bebbaaaa2de65b5834fd77cc5d7 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 2 Aug 2024 15:56:17 +0200 Subject: [PATCH 22/90] Version 5.2.2-customercenter.alpha.3 --- .version | 2 +- CHANGELOG.md | 16 ++++++++++++++++ RevenueCat.podspec | 2 +- RevenueCatUI.podspec | 2 +- Sources/Info.plist | 2 +- Sources/Misc/SystemInfo.swift | 2 +- Tests/BackendIntegrationTestApp/Info.plist | 2 +- Tests/BackendIntegrationTests/Info.plist | 2 +- Tests/UnitTests/Info.plist | 2 +- Tests/UnitTestsHostApp/Info.plist | 2 +- 10 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.version b/.version index 554bfea942..06a1fd8e82 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -5.3.0-SNAPSHOT +5.2.2-customercenter.alpha.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a9057ce1..fbcaadb537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 5.2.2-customercenter.alpha.3 + +- Fix for disabled promo offer button (#4142) + +## 5.2.2-customercenter.alpha.2 + +- Fix project.pbxproj (#4122) +- Fix BackendGetCustomerCenterConfigTests (#4124) +- Add contact support button (#4023) +- Fix checking eligibility (#4138) +- Make colors nullable (#4134) + ## 5.2.2 ### Dependency Updates @@ -12,6 +24,10 @@ - [External] Add missing SwiftUI environment for previews (#4109) via @noahsmartin (#4110) via Andy Boedo (@aboedo) - Remove notify-on-non-patch-release-branches (#4106) via Cesar de la Vega (@vegaro) +## 5.2.1-customercenter.alpha.1 + +- Initial Customer Center Alpha Release + ## 5.2.1 ### Bugfixes * Retry Requests with HTTP Status 429 (#4048) via Will Taylor (@fire-at-will) diff --git a/RevenueCat.podspec b/RevenueCat.podspec index 005a526dee..d41e96e5d6 100644 --- a/RevenueCat.podspec +++ b/RevenueCat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RevenueCat" - s.version = "5.3.0-SNAPSHOT" + s.version = "5.2.2-customercenter.alpha.3" s.summary = "Subscription and in-app-purchase backend service." s.description = <<-DESC diff --git a/RevenueCatUI.podspec b/RevenueCatUI.podspec index 5a3e58e96e..59b73214fe 100644 --- a/RevenueCatUI.podspec +++ b/RevenueCatUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RevenueCatUI" - s.version = "5.3.0-SNAPSHOT" + s.version = "5.2.2-customercenter.alpha.3" s.summary = "UI library for RevenueCat paywalls." s.description = <<-DESC diff --git a/Sources/Info.plist b/Sources/Info.plist index e59670b5f2..05b86430fe 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.0 + 5.2.2 CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index bcf3491960..681d13f258 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -75,7 +75,7 @@ class SystemInfo { } static var frameworkVersion: String { - return "5.3.0-SNAPSHOT" + return "5.2.2-customercenter.alpha.3" } static var systemVersion: String { diff --git a/Tests/BackendIntegrationTestApp/Info.plist b/Tests/BackendIntegrationTestApp/Info.plist index 90886d39ac..351a53a5ca 100644 --- a/Tests/BackendIntegrationTestApp/Info.plist +++ b/Tests/BackendIntegrationTestApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.0 + 5.2.2 CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/Tests/BackendIntegrationTests/Info.plist b/Tests/BackendIntegrationTests/Info.plist index f5c45b69cf..f16bf40066 100644 --- a/Tests/BackendIntegrationTests/Info.plist +++ b/Tests/BackendIntegrationTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.0 + 5.2.2 CFBundleVersion 1 diff --git a/Tests/UnitTests/Info.plist b/Tests/UnitTests/Info.plist index f5c45b69cf..f16bf40066 100644 --- a/Tests/UnitTests/Info.plist +++ b/Tests/UnitTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.0 + 5.2.2 CFBundleVersion 1 diff --git a/Tests/UnitTestsHostApp/Info.plist b/Tests/UnitTestsHostApp/Info.plist index edfe30bc6c..fafe8962b3 100644 --- a/Tests/UnitTestsHostApp/Info.plist +++ b/Tests/UnitTestsHostApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.0 + 5.2.2 CFBundleVersion 1 LSRequiresIPhoneOS From 7e9b3555366567c22cfe5a15a765ac0701136429 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:38:21 +0200 Subject: [PATCH 23/90] [Customer Center] Fixes an extra blank screen after popping FeedbackSurveyView on iOS 18. (#4144) Closes SDK-3519 --- RevenueCatUI/Binding+Extensions.swift | 23 +++++++++++++++++++ .../Views/ManageSubscriptionsView.swift | 10 ++------ 2 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 RevenueCatUI/Binding+Extensions.swift diff --git a/RevenueCatUI/Binding+Extensions.swift b/RevenueCatUI/Binding+Extensions.swift new file mode 100644 index 0000000000..4210b925a5 --- /dev/null +++ b/RevenueCatUI/Binding+Extensions.swift @@ -0,0 +1,23 @@ +// +// 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 +// +// Binding+Extensions.swift +// +// Created by JayShortway on 05/08/2024. + +import SwiftUI + +extension Binding where Value == Bool { + static func isNotNil(_ value: Binding) -> Binding { + Binding( + get: { value.wrappedValue != nil }, + set: { if !$0 { value.wrappedValue = nil } } + ) + } +} diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 8643289ecb..2d9eaa1c05 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -52,12 +52,9 @@ struct ManageSubscriptionsView: View { if #available(iOS 16.0, *) { NavigationStack { content - .navigationDestination(isPresented: .constant(self.viewModel.feedbackSurveyData != nil)) { + .navigationDestination(isPresented: .isNotNil(self.$viewModel.feedbackSurveyData)) { if let feedbackSurveyData = self.viewModel.feedbackSurveyData { FeedbackSurveyView(feedbackSurveyData: feedbackSurveyData) - .onDisappear { - self.viewModel.feedbackSurveyData = nil - } } } }.applyIf(accentColor != nil, apply: { $0.tint(accentColor) }) @@ -67,11 +64,8 @@ struct ManageSubscriptionsView: View { .background(NavigationLink( destination: self.viewModel.feedbackSurveyData.map { data in FeedbackSurveyView(feedbackSurveyData: data) - .onDisappear { - self.viewModel.feedbackSurveyData = nil - } }, - isActive: .constant(self.viewModel.feedbackSurveyData != nil) + isActive: .isNotNil(self.$viewModel.feedbackSurveyData) ) { EmptyView() }) From e1b0cf40c9fbb03aafc8a639c9eca85256e848c1 Mon Sep 17 00:00:00 2001 From: Cody Kerns <44073103+codykerns@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:03:06 -0400 Subject: [PATCH 24/90] [Customer Center] Improve subscription details view (#4152) The subscription details view could take advantage of more available space - expands details view - increases font sizes - wraps in a scrollview for smaller screen sizes - softens shadow - have buttons fill available space, limit width via container views | New | Old | |:-------------:|:-------------:| | ![New Version](https://github.com/user-attachments/assets/27c4a7fd-60be-4536-b88c-b2c9c6141c8f) | ![Old Version](https://github.com/user-attachments/assets/9bd80877-4fa6-4c7e-a813-5e7a3c1bbcc2) | --- .../ManageSubscriptionsButtonStyle.swift | 2 +- .../Views/FeedbackSurveyView.swift | 1 + .../Views/ManageSubscriptionsView.swift | 171 +++++++++++------- .../Views/NoSubscriptionsView.swift | 1 + .../Views/PromotionalOfferView.swift | 1 + 5 files changed, 106 insertions(+), 70 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index d77fe71602..24aea1ecf6 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -33,7 +33,7 @@ struct ManageSubscriptionsButtonStyle: PrimitiveButtonStyle { let textColor = color(from: appearance.buttonTextColor, for: colorScheme) Button(action: { configuration.trigger() }, label: { - configuration.label.frame(width: 300) + configuration.label.frame(maxWidth: .infinity) }) .buttonStyle(.borderedProminent) .controlSize(.large) diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index b7b98abdf4..02336ee508 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -102,6 +102,7 @@ struct FeedbackSurveyButtonsView: View { .disabled(self.loadingState != nil) } } + .padding([.horizontal, .bottom]) } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 2d9eaa1c05..1d2f2ac458 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -80,24 +80,27 @@ struct ManageSubscriptionsView: View { background.edgesIgnoringSafeArea(.all) } - VStack { - if self.viewModel.isLoaded { - HeaderView(viewModel: self.viewModel) - - if let subscriptionInformation = self.viewModel.subscriptionInformation { - SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - localization: self.localization, - refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) - } - - Spacer() + ScrollView { + VStack { + if self.viewModel.isLoaded { + HeaderView(viewModel: self.viewModel) + + if let subscriptionInformation = self.viewModel.subscriptionInformation { + SubscriptionDetailsView( + subscriptionInformation: subscriptionInformation, + localization: self.localization, + refundRequestStatusMessage: self.viewModel.refundRequestStatusMessage) + } - ManageSubscriptionsButtonsView(viewModel: self.viewModel, - loadingPath: self.$viewModel.loadingPath) - } else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) + ManageSubscriptionsButtonsView(viewModel: self.viewModel, + loadingPath: self.$viewModel.loadingPath) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } } + .padding([.horizontal, .bottom]) + .frame(maxWidth: 400) } } .task { @@ -172,78 +175,90 @@ struct SubscriptionDetailsView: View { ) : localization.commonLocalizedString(for: .subExpired) Text("\(explanation)") - .frame(maxWidth: 200, alignment: .leading) - .font(.caption) - .foregroundColor(Color(UIColor.secondaryLabel)) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) }.padding([.bottom], 10) - HStack(alignment: .center) { - Image(systemName: "coloncurrencysign.arrow.circlepath") - .accessibilityHidden(true) - .frame(width: iconWidth) - VStack(alignment: .leading) { - Text(localization.commonLocalizedString(for: .billingCycle)) - .font(.caption2) - .foregroundColor(Color(UIColor.secondaryLabel)) - Text("\(subscriptionInformation.durationTitle)") - .font(.caption) - } - } - - HStack(alignment: .center) { - Image(systemName: "coloncurrencysign") - .accessibilityHidden(true) - .frame(width: iconWidth) - VStack(alignment: .leading) { - Text(localization.commonLocalizedString(for: .currentPrice)) - .font(.caption2) - .foregroundColor(Color(UIColor.secondaryLabel)) - Text("\(subscriptionInformation.price)") - .font(.caption) - } - } - - if let nextRenewal = subscriptionInformation.expirationDateString { - - let expirationString = subscriptionInformation.active ? ( - subscriptionInformation.willRenew ? - localization.commonLocalizedString(for: .nextBillingDate) : - localization.commonLocalizedString(for: .expires) - ) : localization.commonLocalizedString(for: .expired) + Divider() + .padding(.bottom) + VStack(alignment: .leading, spacing: 16.0) { HStack(alignment: .center) { - Image(systemName: "calendar") + Image(systemName: "coloncurrencysign.arrow.circlepath") .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text("\(expirationString)") + Text(localization.commonLocalizedString(for: .billingCycle)) .font(.caption2) - .foregroundColor(Color(UIColor.secondaryLabel)) - Text("\(String(describing: nextRenewal))") - .font(.caption) + .foregroundColor(.secondary) + .textCase(.uppercase) + Text("\(subscriptionInformation.durationTitle)") + .font(.body) } } - } - if let refundRequestStatusMessage = refundRequestStatusMessage { HStack(alignment: .center) { - Image(systemName: "arrowshape.turn.up.backward") + Image(systemName: "coloncurrencysign") .accessibilityHidden(true) .frame(width: iconWidth) VStack(alignment: .leading) { - Text(localization.commonLocalizedString(for: .refundStatus)) + Text(localization.commonLocalizedString(for: .currentPrice)) .font(.caption2) - .foregroundColor(Color(UIColor.secondaryLabel)) - Text("\(refundRequestStatusMessage)") - .font(.caption) + .foregroundColor(.secondary) + .textCase(.uppercase) + Text("\(subscriptionInformation.price)") + .font(.body) + } + } + + if let nextRenewal = subscriptionInformation.expirationDateString { + + let expirationString = subscriptionInformation.active ? ( + subscriptionInformation.willRenew ? + localization.commonLocalizedString(for: .nextBillingDate) : + localization.commonLocalizedString(for: .expires) + ) : localization.commonLocalizedString(for: .expired) + + HStack(alignment: .center) { + Image(systemName: "calendar") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text("\(expirationString)") + .font(.caption2) + .foregroundColor(.secondary) + .textCase(.uppercase) + Text("\(String(describing: nextRenewal))") + .font(.body) + } + } + } + + if let refundRequestStatusMessage = refundRequestStatusMessage { + HStack(alignment: .center) { + Image(systemName: "arrowshape.turn.up.backward") + .accessibilityHidden(true) + .frame(width: iconWidth) + VStack(alignment: .leading) { + Text(localization.commonLocalizedString(for: .refundStatus)) + .font(.caption2) + .foregroundColor(.secondary) + .textCase(.uppercase) + Text("\(refundRequestStatusMessage)") + .font(.body) + } } } } - }.padding() - .padding(.horizontal) - .background(Color(UIColor.tertiarySystemBackground)) - .cornerRadius(20) - .shadow(color: Color.black.opacity(0.2), radius: 4) + + } + .padding(24.0) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(20) + .shadow(color: .black.opacity(0.1), radius: 10, x: 0.0, y: 10) + .padding(.bottom) + .padding(.bottom) } } @@ -354,6 +369,24 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct SubscriptionDetailsView_Previews: PreviewProvider { + + static var previews: some View { + SubscriptionDetailsView( + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformationMonthlyRenewing, + localization: CustomerCenterConfigTestData.customerCenterData.localization, + refundRequestStatusMessage: "Success" + ) + .previewDisplayName("Subscription Details - Monthly") + .padding() + + } +} #endif #endif diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index e1f919bc1d..7d2c7a37fc 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -74,6 +74,7 @@ struct NoSubscriptionsView: View { dismiss() } } + .padding(.horizontal) .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index 2db2ac9062..02e822c647 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -146,6 +146,7 @@ struct PromoOfferButtonView: View { } }) .buttonStyle(ManageSubscriptionsButtonStyle()) + .padding(.horizontal) } } From 92db864a2a2376b9a30e314c57185b4288f2cd43 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Thu, 8 Aug 2024 12:03:34 -0300 Subject: [PATCH 25/90] Customer Center compilation flag (#4149) We have a long-running integration branch for Customer Center, long running integration branches usually get very tricky to merge as time passes on. This PR attempts to solve it by creating a single custom compiler flag that enables the Customer Center feature. Here is what the compilation flag looks like: image Then all we need to do to integrate into main is to delete that flag. And re-add it for development. Easy peasy. And when we want to ship Customer Center for real, we just delete all usages of that flag. If the flag isn't defined, then the code evaluates to false and is simply skipped by the compiler. If it evaluates to true, then the code runs. --------- Co-authored-by: Toni Rico --- Package.swift | 5 +++-- RevenueCat.xcodeproj/project.pbxproj | 1 + .../Abstractions/CustomerCenterPurchasesType.swift | 4 ++++ .../Abstractions/ManageSubscriptionsPurchaseType.swift | 4 ++++ RevenueCatUI/CustomerCenter/ColorFromAppearance.swift | 4 ++++ .../CustomerCenter/CustomerInfo+CurrentEntitlement.swift | 4 ++++ .../CustomerCenter/Data/CustomerCenterAction.swift | 4 ++++ .../Data/CustomerCenterConfigTestData.swift | 4 ++++ .../CustomerCenter/Data/CustomerCenterEnvironment.swift | 4 ++++ .../CustomerCenter/Data/CustomerCenterError.swift | 4 ++++ .../CustomerCenter/Data/CustomerCenterPurchases.swift | 4 ++++ RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift | 4 ++++ .../CustomerCenter/Data/LoadPromotionalOfferUseCase.swift | 4 ++++ .../CustomerCenter/Data/PromotionalOfferData.swift | 4 ++++ .../CustomerCenter/Data/SubscriptionInformation.swift | 4 ++++ .../CustomerCenter/ManageSubscriptionsButtonStyle.swift | 4 ++++ RevenueCatUI/CustomerCenter/URLUtilities.swift | 4 ++++ .../CustomerCenter/View+PresentCustomerCenter.swift | 4 ++++ .../ViewModels/CustomerCenterViewModel.swift | 4 ++++ .../ViewModels/CustomerCenterViewState.swift | 4 ++++ .../ViewModels/FeedbackSurveyViewModel.swift | 4 ++++ .../ViewModels/ManageSubscriptionsViewModel.swift | 4 ++++ .../ViewModels/PromotionalOfferViewModel.swift | 4 ++++ .../CustomerCenter/Views/CustomerCenterView.swift | 4 ++++ .../CustomerCenter/Views/FeedbackSurveyView.swift | 4 ++++ .../CustomerCenter/Views/ManageSubscriptionsView.swift | 4 ++++ .../CustomerCenter/Views/NoSubscriptionsView.swift | 4 ++++ .../CustomerCenter/Views/PromotionalOfferView.swift | 4 ++++ .../CustomerCenter/Views/RestorePurchasesAlert.swift | 4 ++++ .../CustomerCenter/Views/TintedProgressView.swift | 4 ++++ RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift | 4 ++++ Sources/CustomerCenter/CustomerCenterConfigData.swift | 4 ++++ Sources/Purchasing/Purchases/Purchases.swift | 4 ++++ .../SwiftAPITester/CustomerCenterConfigDataAPI.swift | 4 ++++ .../CustomerCenter/CustomerCenterViewModelTests.swift | 4 ++++ .../ManageSubscriptionsViewModelTests.swift | 4 ++++ .../PaywallsTester.xcodeproj/project.pbxproj | 2 +- .../PaywallsTester/UI/Views/SamplePaywallsList.swift | 8 ++++++++ .../CustomerCenter/CustomerCenterConfigDataTests.swift | 4 ++++ .../Backend/BackendGetCustomerCenterConfigTests.swift | 4 ++++ 40 files changed, 157 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 29fc6bcba1..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -75,7 +75,8 @@ let package = Package( // Note: these have to match the values in RevenueCatUI.podspec .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") - ]), + ], + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 822e20b1f4..018af44b08 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6742,6 +6742,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index 3fbce0a20d..80466b67fa 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 18/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -30,3 +32,5 @@ protocol CustomerCenterPurchasesType: Sendable { product: StoreProduct) async throws -> PromotionalOffer } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift index 212f6ca568..21305aeb7e 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 12/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -35,3 +37,5 @@ protocol ManageSubscriptionsPurchaseType: Sendable { func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus } + +#endif diff --git a/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift b/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift index 52f0b17228..5b6a04a07d 100644 --- a/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift +++ b/RevenueCatUI/CustomerCenter/ColorFromAppearance.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 30/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -20,3 +22,5 @@ func color(from colorInformation: CustomerCenterConfigData.Appearance.ColorInfor for colorScheme: ColorScheme) -> Color? { return colorScheme == .dark ? colorInformation.dark?.underlyingColor : colorInformation.light?.underlyingColor } + +#endif diff --git a/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift b/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift index cb9594961d..c18105ec20 100644 --- a/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift +++ b/RevenueCatUI/CustomerCenter/CustomerInfo+CurrentEntitlement.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 17/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -33,3 +35,5 @@ extension CustomerInfo { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift index 18e38ee21c..33dba7a0f6 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterAction.swift @@ -1,3 +1,5 @@ +#if CUSTOMER_CENTER_ENABLED + import RevenueCat /// Typealias for handler for Customer center actions @@ -20,3 +22,5 @@ public enum CustomerCenterAction { case refundRequestCompleted(_ refundRequestStatus: RefundRequestStatus) } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index fb1c5f30ce..9c5e2a967f 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 28/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -130,3 +132,5 @@ enum CustomerCenterConfigTestData { ) } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift index ac4b325dfb..7039ef8e4a 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 19/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -71,3 +73,5 @@ extension EnvironmentValues { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift index 8c8c7ab4d1..3fd3ba5ee7 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 29/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation /// Error produced when displaying the customer center. @@ -44,3 +46,5 @@ extension CustomerCenterError: CustomNSError { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index ead0484128..ad69124d55 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 18/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -35,3 +37,5 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift index c7e85c0fe3..a7e527b563 100644 --- a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift +++ b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 14/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -36,3 +38,5 @@ class FeedbackSurveyData: ObservableObject { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift index 45073dcf66..63001ce6ac 100644 --- a/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift +++ b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 18/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -72,3 +74,5 @@ class LoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift b/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift index 54b6621c5c..f25005c296 100644 --- a/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift +++ b/RevenueCatUI/CustomerCenter/Data/PromotionalOfferData.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 17/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -22,3 +24,5 @@ struct PromotionalOfferData: Identifiable { let promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 8188435f02..9e6781dfa3 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 28/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation struct SubscriptionInformation { @@ -44,3 +46,5 @@ struct SubscriptionInformation { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index 24aea1ecf6..2af8da64c9 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 28/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -58,3 +60,5 @@ struct ManageSubscriptionsButtonStyle_Previews: PreviewProvider { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index 2f79f0f9ba..a2b90f1c03 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 28/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import SwiftUI @@ -42,3 +44,5 @@ enum URLUtilities { #endif } + +#endif diff --git a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift index 10082097a2..769c1072c4 100644 --- a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift +++ b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift @@ -11,6 +11,8 @@ // // Created by Toni Rico Diez on 2024-07-15. +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import SwiftUI @@ -130,3 +132,5 @@ private struct PresentingCustomerCenterModifier: ViewModifier { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 08f35e9bfd..bd97591d10 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 27/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -133,3 +135,5 @@ import RevenueCat } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift index 7a982112d0..639a2185b0 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 11/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation enum CustomerCenterViewState: Equatable { @@ -35,3 +37,5 @@ enum CustomerCenterViewState: Equatable { } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift index 64ca0878a8..f811e91c56 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 17/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -74,3 +76,5 @@ class FeedbackSurveyViewModel: ObservableObject { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index a8ef1d341c..8f08f7124c 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 27/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -246,3 +248,5 @@ private extension SubscriptionPeriod { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift index 6171ff3a17..06452ec80a 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 17/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat @@ -74,3 +76,5 @@ class PromotionalOfferViewModel: ObservableObject { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 2f06147ed2..5ee8fed3b3 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -13,6 +13,8 @@ // Created by Andrés Boedo on 5/3/24. // +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import SwiftUI @@ -122,3 +124,5 @@ struct CustomerCenterView_Previews: PreviewProvider { #endif #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 02336ee508..0205251897 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 12/6/24. // +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import SwiftUI @@ -119,3 +121,5 @@ extension FeedbackSurveyButtonsView { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 1d2f2ac458..83ca0751e0 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -13,6 +13,8 @@ // Created by Andrés Boedo on 5/3/24. // +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import SwiftUI @@ -390,3 +392,5 @@ struct SubscriptionDetailsView_Previews: PreviewProvider { #endif #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 7d2c7a37fc..67c4493a6f 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -13,6 +13,8 @@ // Created by Andrés Boedo on 5/3/24. // +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import SwiftUI @@ -100,3 +102,5 @@ struct NoSubscriptionsView_Previews: PreviewProvider { #endif #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index 02e822c647..8c3a84003a 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 17/6/24. // +#if CUSTOMER_CENTER_ENABLED + import RevenueCat import StoreKit import SwiftUI @@ -217,3 +219,5 @@ private extension SubscriptionPeriod { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index d42004d1ab..0c4e2b155f 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -13,6 +13,8 @@ // Created by Andrés Boedo on 5/3/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -131,3 +133,5 @@ extension View { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift index 1618afef49..91c315ab15 100644 --- a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift +++ b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 19/7/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -56,3 +58,5 @@ struct TintedProgressView_Previews: PreviewProvider { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 8af5eb27d8..5e17d23444 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -13,6 +13,8 @@ // Created by Andrés Boedo on 5/3/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation import RevenueCat import SwiftUI @@ -123,3 +125,5 @@ struct WrongPlatformView_Previews: PreviewProvider { #endif #endif + +#endif diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index feb4490169..e151455dbc 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 28/5/24. // +#if CUSTOMER_CENTER_ENABLED + import Foundation // swiftlint:disable missing_docs nesting file_length @@ -431,3 +433,5 @@ extension CustomerCenterConfigData.Support { } } + +#endif diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 614f768c73..45be32a8b6 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1189,6 +1189,8 @@ public extension Purchases { await self.paywallEventsManager?.track(paywallEvent: paywallEvent) } + #if CUSTOMER_CENTER_ENABLED + /// Used by `RevenueCatUI` to download customer center data func loadCustomerCenter() async throws -> CustomerCenterConfigData { let response = try await Async.call { completion in @@ -1201,6 +1203,8 @@ public extension Purchases { return CustomerCenterConfigData(from: response) } + #endif + /// Used by `RevenueCatUI` to download and cache paywall images. @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *) static let paywallImageDownloadSession: URLSession = PaywallCacheWarming.downloadSession diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift index a0a1ba95f1..4b2414e02d 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/CustomerCenterConfigDataAPI.swift @@ -8,6 +8,8 @@ import Foundation import RevenueCat +#if CUSTOMER_CENTER_ENABLED + func checkCustomerCenterConfigData(_ data: CustomerCenterConfigData) { let screens: [CustomerCenterConfigData.Screen.ScreenType: CustomerCenterConfigData.Screen] = data.screens let appearance: CustomerCenterConfigData.Appearance = data.appearance @@ -95,3 +97,5 @@ func checkPathType(_ type: CustomerCenterConfigData.HelpPath.PathType) { break } } + +#endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 928a10ff94..7a7ba8eca2 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -13,6 +13,8 @@ // Created by Cesar de la Vega on 11/6/24. // +#if CUSTOMER_CENTER_ENABLED + import Nimble import RevenueCat @testable import RevenueCatUI @@ -266,3 +268,5 @@ private extension CustomerCenterViewModelTests { } #endif + +#endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index e7cb1dc9bc..227e035257 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -15,6 +15,8 @@ // swiftlint:disable file_length type_body_length function_body_length +#if CUSTOMER_CENTER_ENABLED + import Nimble @testable import RevenueCat @testable import RevenueCatUI @@ -932,3 +934,5 @@ class MockLoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType { } #endif + +#endif diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index 896ee253b6..245db823ca 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -88,7 +88,9 @@ struct SamplePaywallsList: View { ) ) case .customerCenter: + #if CUSTOMER_CENTER_ENABLED CustomerCenterView() + #endif } } @@ -143,6 +145,7 @@ struct SamplePaywallsList: View { } } + #if CUSTOMER_CENTER_ENABLED #if os(iOS) Section("Customer Center") { Button { @@ -158,14 +161,17 @@ struct SamplePaywallsList: View { } } #endif + #endif } .frame(maxWidth: .infinity) .buttonStyle(.plain) + #if CUSTOMER_CENTER_ENABLED #if os(iOS) .presentCustomerCenter(isPresented: self.$presentingCustomerCenter, customerCenterActionHandler: self.handleCustomerCenterAction) { self.presentingCustomerCenter = false } #endif + #endif } #if os(watchOS) @@ -207,6 +213,7 @@ private struct TemplateLabel: View { // MARK: - +#if CUSTOMER_CENTER_ENABLED #if os(iOS) extension SamplePaywallsList { @@ -229,6 +236,7 @@ extension SamplePaywallsList { } } +#endif #endif private extension SamplePaywallsList { diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index e8dafc45d6..3527e93ecd 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 8/7/24. +#if CUSTOMER_CENTER_ENABLED + import Nimble import XCTest @@ -137,3 +139,5 @@ class CustomerCenterConfigDataTests: TestCase { } } + +#endif diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift index e55a52ef73..574d3e186d 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerCenterConfigTests.swift @@ -11,6 +11,8 @@ // // Created by Cesar de la Vega on 29/6/24. +#if CUSTOMER_CENTER_ENABLED + import Foundation import Nimble import XCTest @@ -380,3 +382,5 @@ private extension BackendGetCustomerCenterConfigTests { ] } + +#endif From c52f3cf7704d4c925821ee413c6b85a32da4ded9 Mon Sep 17 00:00:00 2001 From: Cody Kerns <44073103+codykerns@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:01:12 -0400 Subject: [PATCH 26/90] [Customer Center] UI polish for empty content (#4147) Attempting some UI polish - Implements `ContentUnavailableView` for the views that have 'empty' content (with a visually similar backwards compatible version) - Improves instructions for each platform for wrong-platform subscription management | New | Old | |:-------------:|:-------------:| | ![New Version](https://github.com/user-attachments/assets/866266ae-4996-4480-b990-17bc143641fa) | ![Old Version](https://github.com/user-attachments/assets/936e7892-2d60-4b86-889b-7b1b543d729c) | --- .../CompatibilityContentUnavailableView.swift | 61 +++++++++++++++++++ .../Views/NoSubscriptionsView.swift | 16 ++--- .../Views/WrongPlatformView.swift | 58 +++++++++++++----- 3 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift diff --git a/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift b/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift new file mode 100644 index 0000000000..f534314546 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift @@ -0,0 +1,61 @@ +// +// 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 +// +// CompatibilityContentUnavailableView.swift +// +// +// Created by Cody Kerns on 8/6/24. +// + +import SwiftUI + +#if os(iOS) + +/// A SwiftUI view for displaying a message about unavailable content +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct CompatibilityContentUnavailableView: View { + @State var title: String + @State var description: String + @State var systemImage: String + + var body: some View { + + if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) { + ContentUnavailableView( + title, + systemImage: systemImage, + description: Text(description) + ) + } else { + VStack { + Image(systemName: systemImage) + .resizable() + .scaledToFill() + .frame(width: 48, height: 48) + .foregroundStyle(.secondary) + .padding() + + Text(title) + .font(.title2) + .bold() + + Text(description) + .font(.subheadline) + .foregroundStyle(.secondary) + }.frame(maxHeight: .infinity) + } + + } +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 67c4493a6f..52977ba473 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -51,18 +51,19 @@ struct NoSubscriptionsView: View { let background = color(from: appearance.backgroundColor, for: colorScheme) let textColor = color(from: appearance.textColor, for: colorScheme) + let fallbackDescription = "We can try checking your Apple account for any previous purchases" + ZStack { if background != nil { background.edgesIgnoringSafeArea(.all) } VStack { - Text(self.configuration.screens[.noActive]?.title ?? "No Subscriptions found") - .font(.title) - .padding() - Text(self.configuration.screens[.noActive]?.subtitle ?? - "We can try checking your Apple account for any previous purchases") - .font(.body) - .padding() + CompatibilityContentUnavailableView( + title: self.configuration.screens[.noActive]?.title ?? "No subscriptions found", + description: + self.configuration.screens[.noActive]?.subtitle ?? fallbackDescription, + systemImage: "exclamationmark.triangle.fill" + ) Spacer() @@ -75,6 +76,7 @@ struct NoSubscriptionsView: View { Button(localization.commonLocalizedString(for: .cancel)) { dismiss() } + .padding(.vertical) } .padding(.horizontal) .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 5e17d23444..aa64c48b3d 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -51,20 +51,13 @@ struct WrongPlatformView: View { let textColor = color(from: appearance.textColor, for: colorScheme) VStack { - switch store { - case .appStore, .macAppStore, .playStore, .amazon: - let platformName = humanReadablePlatformName(store: store!) - - Text("Your subscription is a \(platformName) subscription.") - .font(.title) - .padding() - Text("Go the app settings on \(platformName) to manage your subscription and billing.") - .padding() - default: - Text("Please contact support to manage your subscription") - .font(.title) - .padding() - } + let platformInstructions = self.humanReadableInstructions(for: store) + + CompatibilityContentUnavailableView( + title: platformInstructions.0, + description: platformInstructions.1, + systemImage: "exclamationmark.triangle.fill" + ) } .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) @@ -98,6 +91,40 @@ struct WrongPlatformView: View { } } + private func humanReadableInstructions(for store: Store?) -> (String, String) { + let defaultContactSupport = "Please contact support to manage your subscription." + + if let store { + let platformName = humanReadablePlatformName(store: store) + + switch store { + case .appStore, .macAppStore: + return ( + "You have an \(platformName) subscription.", + "You can manage your subscription via the App Store app on an Apple device." + ) + case .playStore: + return ( + "You have a \(platformName) subscription.", + "You can manage your subscription via the Google Play app on an Android device." + ) + case .stripe, .rcBilling, .external: + return ("Active \(platformName) Subscription", defaultContactSupport) + case .promotional: + return ("Active \(platformName) Subscription", defaultContactSupport) + case .amazon: + return ( + "You have an \(platformName) subscription.", + "You can manage your subscription via the Amazon Appstore app." + ) + case .unknownStore: + return ("Unknown Subscription", defaultContactSupport) + } + } else { + return ("Unknown Subscription", defaultContactSupport) + } + } + } #if DEBUG @@ -114,6 +141,9 @@ struct WrongPlatformView_Previews: PreviewProvider { WrongPlatformView(store: .appStore) .previewDisplayName("App Store") + WrongPlatformView(store: .amazon) + .previewDisplayName("Amazon") + WrongPlatformView(store: .rcBilling) .previewDisplayName("RCBilling") } From 36628791edc24565ec68809096fd0685b4995ed4 Mon Sep 17 00:00:00 2001 From: Cody Kerns <44073103+codykerns@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:23:08 -0400 Subject: [PATCH 27/90] [Customer Center] Use system navigation titles (#4161) --- .../Data/CustomerCenterConfigTestData.swift | 8 ++++---- .../Views/FeedbackSurveyView.swift | 8 ++------ .../Views/ManageSubscriptionsView.swift | 20 +++++++++++-------- .../Views/TintedProgressView.swift | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 9c5e2a967f..46270320a4 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -104,11 +104,11 @@ enum CustomerCenterConfigTestData { ) static let standardAppearance = CustomerCenterConfigData.Appearance( - accentColor: .init(light: "#ffffff", dark: "#000000"), + accentColor: .init(light: "#007AFF", dark: "#007AFF"), textColor: .init(light: "#000000", dark: "#ffffff"), - backgroundColor: .init(light: "#000000", dark: "#ffffff"), - buttonTextColor: .init(light: "#000000", dark: "#ffffff"), - buttonBackgroundColor: .init(light: "#000000", dark: "#ffffff") + backgroundColor: .init(light: "#f5f5f7", dark: "#000000"), + buttonTextColor: .init(light: "#ffffff", dark: "#000000"), + buttonBackgroundColor: .init(light: "#287aff", dark: "#287aff") ) static let subscriptionInformationMonthlyRenewing: SubscriptionInformation = .init( diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 0205251897..1f012dc767 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -46,14 +46,8 @@ struct FeedbackSurveyView: View { if let background = color(from: appearance.backgroundColor, for: colorScheme) { background.edgesIgnoringSafeArea(.all) } - let textColor = color(from: appearance.textColor, for: colorScheme) VStack { - Text(self.viewModel.feedbackSurveyData.configuration.title) - .font(.title) - .padding() - .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) - Spacer() FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options, @@ -69,6 +63,8 @@ struct FeedbackSurveyView: View { promoOfferDetails: promotionalOfferData.promoOfferDetails) }) } + .navigationTitle(self.viewModel.feedbackSurveyData.configuration.title) + .navigationBarTitleDisplayMode(.inline) } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 83ca0751e0..a35d2bd6f8 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -85,7 +85,7 @@ struct ManageSubscriptionsView: View { ScrollView { VStack { if self.viewModel.isLoaded { - HeaderView(viewModel: self.viewModel) + SubtitleTextView(subtitle: self.viewModel.screen.subtitle) if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView( @@ -108,6 +108,7 @@ struct ManageSubscriptionsView: View { .task { await loadInformationIfNeeded() } + .navigationTitle(self.viewModel.screen.title) .navigationBarTitleDisplayMode(.inline) } } @@ -132,10 +133,9 @@ private extension ManageSubscriptionsView { @available(tvOS, unavailable) @available(watchOS, unavailable) @available(visionOS, unavailable) -struct HeaderView: View { +struct SubtitleTextView: View { - @ObservedObject - private(set) var viewModel: ManageSubscriptionsViewModel + private(set) var subtitle: String? @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance @Environment(\.colorScheme) @@ -144,10 +144,13 @@ struct HeaderView: View { var body: some View { let textColor = color(from: appearance.textColor, for: colorScheme) - Text(self.viewModel.screen.title) - .font(.title) - .padding() - .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) + if let subtitle { + Text(subtitle) + .font(.subheadline) + .padding([.horizontal]) + .multilineTextAlignment(.center) + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) + } } } @@ -259,6 +262,7 @@ struct SubscriptionDetailsView: View { .background(Color(UIColor.tertiarySystemBackground)) .cornerRadius(20) .shadow(color: .black.opacity(0.1), radius: 10, x: 0.0, y: 10) + .padding(.top) .padding(.bottom) .padding(.bottom) } diff --git a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift index 91c315ab15..9100a26ec8 100644 --- a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift +++ b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift @@ -32,7 +32,7 @@ struct TintedProgressView: View { var body: some View { ProgressView() .controlSize(.regular) - .tint(colorScheme == .dark ? Color.black : Color.white) + .tint(colorScheme == .light ? Color.black : Color.white) } } From 92841076720430ec712fcd9c7f547007f024bd7a Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 09:06:55 +0200 Subject: [PATCH 28/90] Disable `CustomerCenter` build flags (#4160) ### Description This disables CustomerCenter components from the integration branch so it can be merged into `main`. Then, we can revert this into a new branch, which would be used to test CustomerCenter --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 - .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8feea04c3d..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), + swiftSettings: []), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 45d5a2d15a..fe7ba98c15 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6782,7 +6782,6 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From 759fd53a0c34ca2d51037c04a845db6a2bf3b335 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 09:19:41 +0200 Subject: [PATCH 29/90] Fix compilation issues in older targets --- .../CustomerCenter/Data/LoadPromotionalOfferUseCase.swift | 1 - RevenueCatUI/CustomerCenter/URLUtilities.swift | 1 - .../CustomerCenter/View+PresentCustomerCenter.swift | 1 - .../ViewModels/PromotionalOfferViewModel.swift | 1 - .../Views/CompatibilityContentUnavailableView.swift | 3 ++- .../CustomerCenter/Views/CustomerCenterView.swift | 3 --- .../CustomerCenter/Views/FeedbackSurveyView.swift | 3 --- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 8 -------- .../CustomerCenter/Views/NoSubscriptionsView.swift | 2 -- .../CustomerCenter/Views/PromotionalOfferView.swift | 3 --- .../CustomerCenter/Views/RestorePurchasesAlert.swift | 3 --- .../CustomerCenter/Views/TintedProgressView.swift | 2 -- RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift | 2 -- 13 files changed, 2 insertions(+), 31 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift index 63001ce6ac..d476c8307b 100644 --- a/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift +++ b/RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift @@ -30,7 +30,6 @@ protocol LoadPromotionalOfferUseCaseType { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) @MainActor class LoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType { diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index a2b90f1c03..e2c5e93b42 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -26,7 +26,6 @@ enum URLUtilities { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) - @available(visionOS, unavailable) static func createMailURLIfPossible(email: String, subject: String, body: String) -> URL? { let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" diff --git a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift index 769c1072c4..2f682975d7 100644 --- a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift +++ b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift @@ -77,7 +77,6 @@ extension View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) private struct PresentingCustomerCenterModifier: ViewModifier { let customerCenterActionHandler: CustomerCenterActionHandler? diff --git a/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift index 06452ec80a..82bbabc6c7 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift @@ -24,7 +24,6 @@ import RevenueCat @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) @MainActor class PromotionalOfferViewModel: ObservableObject { diff --git a/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift b/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift index f534314546..b9603466ae 100644 --- a/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CompatibilityContentUnavailableView.swift @@ -15,6 +15,7 @@ import SwiftUI +#if CUSTOMER_CENTER_ENABLED #if os(iOS) /// A SwiftUI view for displaying a message about unavailable content @@ -22,7 +23,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct CompatibilityContentUnavailableView: View { @State var title: String @State var description: String @@ -59,3 +59,4 @@ struct CompatibilityContentUnavailableView: View { } #endif +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 5ee8fed3b3..33d2fafef5 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -25,7 +25,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel @@ -78,7 +77,6 @@ public struct CustomerCenterView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) private extension CustomerCenterView { func loadInformationIfNeeded() async { @@ -111,7 +109,6 @@ private extension CustomerCenterView { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct CustomerCenterView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 1f012dc767..6d2ec236f6 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -24,7 +24,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct FeedbackSurveyView: View { @StateObject @@ -73,7 +72,6 @@ struct FeedbackSurveyView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct FeedbackSurveyButtonsView: View { let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] @@ -109,7 +107,6 @@ struct FeedbackSurveyButtonsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) extension FeedbackSurveyButtonsView { private static let buttonSpacing: CGFloat = 16 diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index a35d2bd6f8..1ed6a4de94 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -24,7 +24,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct ManageSubscriptionsView: View { @Environment(\.appearance) @@ -117,7 +116,6 @@ struct ManageSubscriptionsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) private extension ManageSubscriptionsView { func loadInformationIfNeeded() async { @@ -132,7 +130,6 @@ private extension ManageSubscriptionsView { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct SubtitleTextView: View { private(set) var subtitle: String? @@ -159,7 +156,6 @@ struct SubtitleTextView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct SubscriptionDetailsView: View { let iconWidth = 22.0 @@ -273,7 +269,6 @@ struct SubscriptionDetailsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct ManageSubscriptionsButtonsView: View { @ObservedObject @@ -307,7 +302,6 @@ struct ManageSubscriptionsButtonsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct ManageSubscriptionButton: View { let path: CustomerCenterConfigData.HelpPath @@ -348,7 +342,6 @@ struct ManageSubscriptionButton: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { @@ -379,7 +372,6 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct SubscriptionDetailsView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 52977ba473..871e80d34e 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -24,7 +24,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct NoSubscriptionsView: View { // swiftlint:disable:next todo @@ -92,7 +91,6 @@ struct NoSubscriptionsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct NoSubscriptionsView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index 8c3a84003a..3b8dc97ff1 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -25,7 +25,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct PromotionalOfferView: View { @StateObject @@ -83,7 +82,6 @@ struct PromotionalOfferView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct PromotionalOfferHeaderView: View { @Environment(\.appearance) @@ -114,7 +112,6 @@ struct PromotionalOfferHeaderView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct PromoOfferButtonView: View { @Environment(\.locale) diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 0c4e2b155f..9175ac679a 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -25,7 +25,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct RestorePurchasesAlert: ViewModifier { @Binding @@ -107,7 +106,6 @@ struct RestorePurchasesAlert: ViewModifier { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) private extension RestorePurchasesAlert { func setAlertType(_ newType: AlertType) { @@ -123,7 +121,6 @@ private extension RestorePurchasesAlert { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) extension View { func restorePurchasesAlert(isPresented: Binding) -> some View { diff --git a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift index 9100a26ec8..d9b9c75cf5 100644 --- a/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift +++ b/RevenueCatUI/CustomerCenter/Views/TintedProgressView.swift @@ -23,7 +23,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct TintedProgressView: View { @Environment(\.appearance) private var appearance: CustomerCenterConfigData.Appearance @@ -41,7 +40,6 @@ struct TintedProgressView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct TintedProgressView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index aa64c48b3d..4fff043cd9 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -25,7 +25,6 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct WrongPlatformView: View { @State @@ -133,7 +132,6 @@ struct WrongPlatformView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@available(visionOS, unavailable) struct WrongPlatformView_Previews: PreviewProvider { static var previews: some View { From ae44b59c2307f58136041500c6d064fdb65fcb90 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:37:55 +0200 Subject: [PATCH 30/90] Adds SemanticVersion --- RevenueCat.xcodeproj/project.pbxproj | 8 +++ .../CustomerCenter/Data/SemanticVersion.swift | 69 ++++++++++++++++++ .../Data/SemanticVersionTests.swift | 71 +++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift create mode 100644 Tests/RevenueCatUITests/Data/SemanticVersionTests.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index fe7ba98c15..297a7cfc72 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -661,6 +661,8 @@ 57FFD2512922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; }; 57FFD2522922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; }; 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; }; + 77632EE32C64E41100634647 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77632EE22C64E40D00634647 /* SemanticVersion.swift */; }; + 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; 80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; }; 8834AFA52C2B9375005A72FE /* PresentIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62222C1D168B00E1A461 /* PresentIfNeededTests.swift */; }; @@ -1787,6 +1789,8 @@ 57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxEnvironmentDetectorTests.swift; sourceTree = ""; }; 57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSandboxEnvironmentDetector.swift; sourceTree = ""; }; 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; + 77632EE22C64E40D00634647 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; + 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; 84C3F1AC1D7E1E64341D3936 /* Pods_RevenueCat_PurchasesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat_PurchasesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 887A5FB42C1D024300E1A461 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -3103,6 +3107,7 @@ 353756562C382C2800A1B8D6 /* Data */ = { isa = PBXGroup; children = ( + 77632EE22C64E40D00634647 /* SemanticVersion.swift */, 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */, 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, @@ -4089,6 +4094,7 @@ 887A612A2C1D168B00E1A461 /* PaywallDataValidationTests.swift */, 887A612B2C1D168B00E1A461 /* TemplateViewConfigurationTests.swift */, 887A612C2C1D168B00E1A461 /* VariablesTests.swift */, + 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */, ); path = Data; sourceTree = ""; @@ -5876,6 +5882,7 @@ 887A60C42C1D037000E1A461 /* IconView.swift in Sources */, 887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */, 887A606F2C1D037000E1A461 /* PaywallData+Validation.swift in Sources */, + 77632EE32C64E41100634647 /* SemanticVersion.swift in Sources */, 88A543DF2C37A45B0039C6A5 /* TemplatePackageSetting.swift in Sources */, 3546355D2C391F38001D7E85 /* PromotionalOfferViewModel.swift in Sources */, 35F249CA2C493D970058993A /* LoadPromotionalOfferUseCase.swift in Sources */, @@ -5909,6 +5916,7 @@ 887A633A2C1D177800E1A461 /* PaywallViewDynamicTypeTests.swift in Sources */, 887A633B2C1D177800E1A461 /* PaywallViewLocalizationTests.swift in Sources */, 887A633C2C1D177800E1A461 /* Template1ViewTests.swift in Sources */, + 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */, 88AD4C482C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift in Sources */, 887A633D2C1D177800E1A461 /* Template2ViewTests.swift in Sources */, 887A633E2C1D177800E1A461 /* Template3ViewTests.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift new file mode 100644 index 0000000000..5cef4eb1c1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift @@ -0,0 +1,69 @@ +// +// 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 +// +// SemanticVersion.swift +// +// Created by JayShortway on 08/08/2024. + +import Foundation + +struct SemanticVersion: Comparable { + let major: Int + let minor: Int + let patch: Int + + init(major: Int, minor: Int, patch: Int) { + self.major = major + self.minor = minor + self.patch = patch + } + + init(_ version: String) throws { + let pattern = #"^(\d+)(?:\.(\d+))?(?:\.(\d+))?$"# + guard let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch(in: version, range: NSRange(version.startIndex..., in: version)) else { + throw SemanticVersionError.invalidVersionString(version) + } + + let major = Int(version[Range(match.range(at: 1), in: version)!])! + let minor = match.range(at: 2).location != NSNotFound ? + Int(version[Range(match.range(at: 2), in: version)!])! : + 0 + let patch = match.range(at: 3).location != NSNotFound ? + Int(version[Range(match.range(at: 3), in: version)!])! : + 0 + + self.init(major: major, minor: minor, patch: patch) + } + + static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool { + if lhs.major != rhs.major { + return lhs.major < rhs.major + } else if lhs.minor != rhs.minor { + return lhs.minor < rhs.minor + } else { + return lhs.patch < rhs.patch + } + } + + static func == (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool { + return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch + } +} + +enum SemanticVersionError: LocalizedError { + case invalidVersionString(String) + + var errorDescription: String? { + switch self { + case .invalidVersionString(let version): + return "Invalid version string: '\(version)'. Expected format: 'major.minor.patch'" + } + } +} diff --git a/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift new file mode 100644 index 0000000000..b29ba9892f --- /dev/null +++ b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift @@ -0,0 +1,71 @@ +// +// 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 +// +// SemanticVersionTests.swift +// +// Created by JayShortway on 09/08/2024. + +import Nimble +@testable import RevenueCatUI +import XCTest + +class SemanticVersionTests: TestCase { + + func testValidVersionString() { + let testCases = [ + (string: "12.4503.2", major: 12, minor: 4503, patch: 2), + (string: "12.4503", major: 12, minor: 4503, patch: 0), + (string: "12", major: 12, minor: 0, patch: 0), + (string: "12.0.1", major: 12, minor: 0, patch: 1), + (string: "0.0.1", major: 0, minor: 0, patch: 1), + (string: "0.1.0", major: 0, minor: 1, patch: 0), + (string: "1.0.0", major: 1, minor: 0, patch: 0), + (string: "0.0.0", major: 0, minor: 0, patch: 0), + (string: "1.0", major: 1, minor: 0, patch: 0) + ] + for (string, major, minor, patch) in testCases { + XCTContext.runActivity( + named: "Should correctly parse version: '\(string)'" + ) { _ in + // swiftlint:disable:next force_try + let actual = try! SemanticVersion(string) + let expected = SemanticVersion(major: major, minor: minor, patch: patch) + + expect(actual) == expected + } + } + } + + func testInvalidVersionString() { + let testCases = [ + "12.4503.2.3", + "-12.4503.2", + "12.-4503.2", + "12.4503.-2", + "-12", + "-12.-4503", + "-12.-4503.-2", + "12.4503.2 and some more text", + "Some more text and 12.4503.2", + "", + "some.text.whoa", + "sometextwhoa", + "1.text.whoa" + ] + for (string) in testCases { + XCTContext.runActivity( + named: "Should fail to parse version: '\(string)'" + ) { _ in + expect { try SemanticVersion(string) } + .to(throwError(SemanticVersionError.invalidVersionString(string))) + } + } + } + +} From 52195fb37a08e8da91569a6af6b1ec1d9e76d16f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:30:44 +0200 Subject: [PATCH 31/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 + .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2c9cb2513e..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: []), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 297a7cfc72..c0e10cbd04 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6790,6 +6790,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From f1e4da2ef4bf48e716b775f6347c305f1c35c79d Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:23:17 +0200 Subject: [PATCH 32/90] CustomerCenterViewModel checks whether the app is the latest version. --- .../Data/CustomerCenterConfigTestData.swift | 169 +++++++++--------- .../ViewModels/CustomerCenterViewModel.swift | 81 ++++++--- .../CustomerCenterViewModelTests.swift | 47 +++++ 3 files changed, 193 insertions(+), 104 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 46270320a4..1a2b07e357 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -21,87 +21,94 @@ import RevenueCat enum CustomerCenterConfigTestData { @available(iOS 14.0, *) - static let customerCenterData = CustomerCenterConfigData( - screens: [.management: - .init( - type: .management, - title: "Manage Subscription", - subtitle: "Manage your subscription details here", - paths: [ - .init( - id: "1", - title: "Didn't receive purchase", - type: .missingPurchase, - detail: nil - ), - .init( - id: "2", - title: "Request a refund", - type: .refundRequest, - detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer( - iosOfferId: "offer_id", - eligible: true, - title: "title", - subtitle: "subtitle" - )) - ), - .init( - id: "3", - title: "Change plans", - type: .changePlans, - detail: nil - ), - .init( - id: "4", - title: "Cancel subscription", - type: .cancel, - detail: .feedbackSurvey(.init( - title: "Why are you cancelling?", - options: [ - .init( - id: "1", - title: "Too expensive", - promotionalOffer: nil - ), - .init( - id: "2", - title: "Don't use the app", - promotionalOffer: nil - ), - .init( - id: "3", - title: "Bought by mistake", - promotionalOffer: nil - ) - ] - )) - ) - ] - ), - .noActive: .init( - type: .noActive, - title: "No Active Subscription", - subtitle: "You currently have no active subscriptions", - paths: [ - .init( - id: "9q9719171o", - title: "Check purchases", - type: .missingPurchase, - detail: nil - ) - ] - ) - ], - appearance: standardAppearance, - localization: .init( - locale: "en_US", - localizedStrings: [ - "cancel": "Cancel", - "back": "Back" - ] - ), - support: .init(email: "test-support@revenuecat.com") - ) + // swiftlint:disable:next function_body_length + static func customerCenterData(lastPublishedAppVersion: String) -> CustomerCenterConfigData { + CustomerCenterConfigData( + screens: [.management: + .init( + type: .management, + title: "Manage Subscription", + subtitle: "Manage your subscription details here", + paths: [ + .init( + id: "1", + title: "Didn't receive purchase", + type: .missingPurchase, + detail: nil + ), + .init( + id: "2", + title: "Request a refund", + type: .refundRequest, + detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer( + iosOfferId: "offer_id", + eligible: true, + title: "title", + subtitle: "subtitle" + )) + ), + .init( + id: "3", + title: "Change plans", + type: .changePlans, + detail: nil + ), + .init( + id: "4", + title: "Cancel subscription", + type: .cancel, + detail: .feedbackSurvey(.init( + title: "Why are you cancelling?", + options: [ + .init( + id: "1", + title: "Too expensive", + promotionalOffer: nil + ), + .init( + id: "2", + title: "Don't use the app", + promotionalOffer: nil + ), + .init( + id: "3", + title: "Bought by mistake", + promotionalOffer: nil + ) + ] + )) + ) + ] + ), + .noActive: .init( + type: .noActive, + title: "No Active Subscription", + subtitle: "You currently have no active subscriptions", + paths: [ + .init( + id: "9q9719171o", + title: "Check purchases", + type: .missingPurchase, + detail: nil + ) + ] + ) + ], + appearance: standardAppearance, + localization: .init( + locale: "en_US", + localizedStrings: [ + "cancel": "Cancel", + "back": "Back" + ] + ), + support: .init(email: "test-support@revenuecat.com"), + lastPublishedAppVersion: lastPublishedAppVersion + ) + } + + @available(iOS 14.0, *) + static let customerCenterData = customerCenterData(lastPublishedAppVersion: "1.0.0") static let standardAppearance = CustomerCenterConfigData.Appearance( accentColor: .init(light: "#007AFF", dark: "#007AFF"), diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index bd97591d10..afc8edd4a5 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -27,11 +27,16 @@ import RevenueCat @MainActor class CustomerCenterViewModel: ObservableObject { typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo + typealias CurrentVersionFetcher = () -> String? + + private lazy var currentAppVersion: String? = currentVersionFetcher() @Published private(set) var hasSubscriptions: Bool = false @Published private(set) var subscriptionsAreFromApple: Bool = false + @Published + private(set) var appIsLatestVersion: Bool = false // @PublicForExternalTesting @Published @@ -43,50 +48,66 @@ import RevenueCat } } @Published - var configuration: CustomerCenterConfigData? + var configuration: CustomerCenterConfigData? { + didSet { + // We fail open. + let defaultAppIsLatestVersion = true + + guard let currentVersionString = currentAppVersion?.takeVersion() else { + self.appIsLatestVersion = defaultAppIsLatestVersion + return + } + guard let latestVersionString = configuration?.lastPublishedAppVersion.takeVersion() else { + self.appIsLatestVersion = defaultAppIsLatestVersion + return + } + + do { + let currentVersion = try SemanticVersion(currentVersionString) + let latestVersion = try SemanticVersion(latestVersionString) + self.appIsLatestVersion = currentVersion >= latestVersion + } catch { + self.appIsLatestVersion = defaultAppIsLatestVersion + } + } + } var isLoaded: Bool { return state != .notLoaded && configuration != nil } private var customerInfoFetcher: CustomerInfoFetcher + private let currentVersionFetcher: CurrentVersionFetcher internal let customerCenterActionHandler: CustomerCenterActionHandler? private var error: Error? - convenience init(customerCenterActionHandler: CustomerCenterActionHandler?) { - self.init(customerCenterActionHandler: customerCenterActionHandler, - customerInfoFetcher: { - guard Purchases.isConfigured else { - throw PaywallError.purchasesNotConfigured - } - + init( + customerCenterActionHandler: CustomerCenterActionHandler?, + customerInfoFetcher: @escaping CustomerInfoFetcher = { + guard Purchases.isConfigured else { throw PaywallError.purchasesNotConfigured } return try await Purchases.shared.customerInfo() - }) - } - - init(customerCenterActionHandler: CustomerCenterActionHandler?, - customerInfoFetcher: @escaping CustomerInfoFetcher) { + }, + currentVersionFetcher: @escaping CurrentVersionFetcher = { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String + } + ) { self.state = .notLoaded self.customerInfoFetcher = customerInfoFetcher + self.currentVersionFetcher = currentVersionFetcher self.customerCenterActionHandler = customerCenterActionHandler } #if DEBUG - init(hasSubscriptions: Bool = false, - areSubscriptionsFromApple: Bool = false) { + convenience init( + hasSubscriptions: Bool = false, + areSubscriptionsFromApple: Bool = false + ) { + self.init(customerCenterActionHandler: nil) self.hasSubscriptions = hasSubscriptions self.subscriptionsAreFromApple = areSubscriptionsFromApple - self.customerInfoFetcher = { - guard Purchases.isConfigured else { - throw PaywallError.purchasesNotConfigured - } - - return try await Purchases.shared.customerInfo() - } self.state = .success - self.customerCenterActionHandler = nil } #endif @@ -131,7 +152,21 @@ import RevenueCat return .purchasesNotFound } } +} +fileprivate extension String { + /// Takes the first characters of this string, if they conform to Major.Minor.Patch. Returns nil otherwise. + /// Note that Minor and Patch are optional. So if this string starts with a single number, that number is returned. + func takeVersion() -> String? { + do { + let pattern = #"^(\d+)(?:\.\d+)?(?:\.\d+)?"# + let regex = try NSRegularExpression(pattern: pattern) + let match = regex.firstMatch(in: self, range: NSRange(self.startIndex..., in: self)) + return match.map { String(self[Range($0.range, in: self)!]) } + } catch { + return nil + } + } } #endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 7a7ba8eca2..3b52e41eaf 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -128,6 +128,53 @@ class CustomerCenterViewModelTests: TestCase { } } + func testAppIsLatestVersion() { + let testCases = [ + (currentVersion: "1.0.0", latestVersion: "2.0.0", expectedAppIsLatestVersion: false), + (currentVersion: "2.0.0", latestVersion: "2.0.0", expectedAppIsLatestVersion: true), + (currentVersion: "3.0.0", latestVersion: "2.0.0", expectedAppIsLatestVersion: true), + (currentVersion: "1.0.0", latestVersion: "1.1.0", expectedAppIsLatestVersion: false), + (currentVersion: "1.1.0", latestVersion: "1.1.0", expectedAppIsLatestVersion: true), + (currentVersion: "1.1.0", latestVersion: "1.0.0", expectedAppIsLatestVersion: true), + (currentVersion: "1.0.0", latestVersion: "1.0.1", expectedAppIsLatestVersion: false), + (currentVersion: "1.0.1", latestVersion: "1.0.1", expectedAppIsLatestVersion: true), + (currentVersion: "1.0.1", latestVersion: "1.0.0", expectedAppIsLatestVersion: true), + // The CFBundleVersion docs state: + // > You can include more integers but the system ignores them. + // https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion + // So we should do the same. + (currentVersion: "2.0.0.2.3.4", latestVersion: "2.0.0.3.4.5", expectedAppIsLatestVersion: true), + (currentVersion: "1.0.0.2.3.4", latestVersion: "2.0.0.3.4.5", expectedAppIsLatestVersion: false), + (currentVersion: "1.2", latestVersion: "2", expectedAppIsLatestVersion: false), + (currentVersion: "1.2", latestVersion: "1", expectedAppIsLatestVersion: true), + (currentVersion: "2", latestVersion: "1", expectedAppIsLatestVersion: true), + (currentVersion: "0", latestVersion: "1", expectedAppIsLatestVersion: false), + (currentVersion: "10.2", latestVersion: "2", expectedAppIsLatestVersion: true), + // We default to true if we fail to parse any of the two versions. + (currentVersion: "not-a-number", latestVersion: "not-a-number-either", expectedAppIsLatestVersion: true), + (currentVersion: "not-a-number", latestVersion: "1.2.3", expectedAppIsLatestVersion: true), + (currentVersion: "1.2.3", latestVersion: "not-a-number", expectedAppIsLatestVersion: true) + ] + for (currentVersion, latestVersion, expectedAppIsLatestVersion) in testCases { + XCTContext.runActivity( + named: "Current version = \(currentVersion), " + + "latest version = \(latestVersion), " + + "expectedAppIsLatestVersion = \(expectedAppIsLatestVersion)" + ) { _ in + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return currentVersion } + ) + viewModel.state = .success + viewModel.configuration = CustomerCenterConfigTestData.customerCenterData( + lastPublishedAppVersion: latestVersion + ) + + expect(viewModel.appIsLatestVersion) == expectedAppIsLatestVersion + } + } + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) From 08000e5e7c4d6f54aa9e9244b40430050e53a1c6 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:06:53 +0200 Subject: [PATCH 33/90] Cleanup --- .version | 2 +- Package.swift | 2 +- RevenueCat.podspec | 2 +- RevenueCat.xcodeproj/project.pbxproj | 1 - RevenueCatUI.podspec | 2 +- Sources/Misc/SystemInfo.swift | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.version b/.version index 06a1fd8e82..554bfea942 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -5.2.2-customercenter.alpha.3 +5.3.0-SNAPSHOT diff --git a/Package.swift b/Package.swift index 6ccfcdc96f..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], diff --git a/RevenueCat.podspec b/RevenueCat.podspec index d41e96e5d6..005a526dee 100644 --- a/RevenueCat.podspec +++ b/RevenueCat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RevenueCat" - s.version = "5.2.2-customercenter.alpha.3" + s.version = "5.3.0-SNAPSHOT" s.summary = "Subscription and in-app-purchase backend service." s.description = <<-DESC diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index fc171fe53d..13f1d008ae 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6789,7 +6789,6 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/RevenueCatUI.podspec b/RevenueCatUI.podspec index 59b73214fe..5a3e58e96e 100644 --- a/RevenueCatUI.podspec +++ b/RevenueCatUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RevenueCatUI" - s.version = "5.2.2-customercenter.alpha.3" + s.version = "5.3.0-SNAPSHOT" s.summary = "UI library for RevenueCat paywalls." s.description = <<-DESC diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index 681d13f258..bcf3491960 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -75,7 +75,7 @@ class SystemInfo { } static var frameworkVersion: String { - return "5.2.2-customercenter.alpha.3" + return "5.3.0-SNAPSHOT" } static var systemVersion: String { From 9d5cffbc45769caaa31715e46392b30dfdb51228 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:08:50 +0200 Subject: [PATCH 34/90] More cleanup. --- .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From dc1f795b78a09036328b8106d918d32049895bfe Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:29:19 +0200 Subject: [PATCH 35/90] CustomerCenterConfigResponse now provides lastPublishedAppVersion. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 2 +- Sources/CustomerCenter/CustomerCenterConfigData.swift | 6 +++++- .../Networking/Responses/CustomerCenterConfigResponse.swift | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index afc8edd4a5..81278dab25 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -57,7 +57,7 @@ import RevenueCat self.appIsLatestVersion = defaultAppIsLatestVersion return } - guard let latestVersionString = configuration?.lastPublishedAppVersion.takeVersion() else { + guard let latestVersionString = configuration?.lastPublishedAppVersion?.takeVersion() else { self.appIsLatestVersion = defaultAppIsLatestVersion return } diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index e151455dbc..4844b77653 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -26,15 +26,18 @@ public struct CustomerCenterConfigData { public let appearance: Appearance public let localization: Localization public let support: Support + public let lastPublishedAppVersion: String? public init(screens: [Screen.ScreenType: Screen], appearance: Appearance, localization: Localization, - support: Support) { + support: Support, + lastPublishedAppVersion: String?) { self.screens = screens self.appearance = appearance self.localization = localization self.support = support + self.lastPublishedAppVersion = lastPublishedAppVersion } public struct Localization { @@ -329,6 +332,7 @@ extension CustomerCenterConfigData { return (type, Screen(from: $0.value, localization: localization)) }) self.support = Support(from: response.customerCenter.support) + self.lastPublishedAppVersion = response.lastPublishedAppVersion } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index ad9c362534..6690badaac 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -20,6 +20,7 @@ import Foundation struct CustomerCenterConfigResponse { let customerCenter: CustomerCenter + let lastPublishedAppVersion: String? struct CustomerCenter { From be6564049c536563de3da8cf3ff5907c52bc8c8e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:05:13 +0200 Subject: [PATCH 36/90] Fixes CustomerCenterConfigDataTests. --- .../CustomerCenter/CustomerCenterConfigDataTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index 3527e93ecd..eea10e6bbf 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -82,7 +82,8 @@ class CustomerCenterConfigDataTests: TestCase { ], localization: .init(locale: "en_US", localizedStrings: ["key": "value"]), support: .init(email: "support@example.com") - ) + ), + lastPublishedAppVersion: "1.2.3" ) let configData = CustomerCenterConfigData(from: mockResponse) @@ -136,6 +137,8 @@ class CustomerCenterConfigDataTests: TestCase { } else { fail("Expected feedbackSurvey detail") } + + expect(configData.lastPublishedAppVersion) == "1.2.3" } } From 507d91c0c9144ca50102c5e19b7a400a5d220265 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:05:49 +0200 Subject: [PATCH 37/90] Adds some more test cases. --- .../CustomerCenter/CustomerCenterViewModelTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 3b52e41eaf..caaa4b4f57 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -153,7 +153,10 @@ class CustomerCenterViewModelTests: TestCase { // We default to true if we fail to parse any of the two versions. (currentVersion: "not-a-number", latestVersion: "not-a-number-either", expectedAppIsLatestVersion: true), (currentVersion: "not-a-number", latestVersion: "1.2.3", expectedAppIsLatestVersion: true), - (currentVersion: "1.2.3", latestVersion: "not-a-number", expectedAppIsLatestVersion: true) + (currentVersion: "1.2.3", latestVersion: "not-a-number", expectedAppIsLatestVersion: true), + (currentVersion: nil, latestVersion: nil, expectedAppIsLatestVersion: true), + (currentVersion: "1.2.3", latestVersion: nil, expectedAppIsLatestVersion: true), + (currentVersion: nil, latestVersion: "1.2.3", expectedAppIsLatestVersion: true) ] for (currentVersion, latestVersion, expectedAppIsLatestVersion) in testCases { XCTContext.runActivity( From 71171ac851e1ad1b2bd3e563bdd40047c9662462 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:16:19 +0200 Subject: [PATCH 38/90] Correctly (?) adds SemanticVersion to Xcode. --- RevenueCat.xcodeproj/project.pbxproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 13f1d008ae..d64dec5895 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -661,7 +661,7 @@ 57FFD2512922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; }; 57FFD2522922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; }; 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; }; - 77632EE32C64E41100634647 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77632EE22C64E40D00634647 /* SemanticVersion.swift */; }; + 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */; }; 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; 80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; }; @@ -1789,7 +1789,7 @@ 57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxEnvironmentDetectorTests.swift; sourceTree = ""; }; 57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSandboxEnvironmentDetector.swift; sourceTree = ""; }; 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; - 77632EE22C64E40D00634647 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; + 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; 84C3F1AC1D7E1E64341D3936 /* Pods_RevenueCat_PurchasesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat_PurchasesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -3107,6 +3107,7 @@ 353756562C382C2800A1B8D6 /* Data */ = { isa = PBXGroup; children = ( + 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */, 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */, 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, @@ -5794,6 +5795,7 @@ 887A60CC2C1D037000E1A461 /* PaywallFontProvider.swift in Sources */, 887A60B82C1D037000E1A461 /* Template1View.swift in Sources */, 887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */, + 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */, 887A60C72C1D037000E1A461 /* PackageButtonStyle.swift in Sources */, 887A60C52C1D037000E1A461 /* IntroEligibilityStateView.swift in Sources */, 88A543E72C37A4C40039C6A5 /* TierSelectorView.swift in Sources */, @@ -5881,7 +5883,6 @@ 887A60C42C1D037000E1A461 /* IconView.swift in Sources */, 887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */, 887A606F2C1D037000E1A461 /* PaywallData+Validation.swift in Sources */, - 77632EE32C64E41100634647 /* SemanticVersion.swift in Sources */, 88A543DF2C37A45B0039C6A5 /* TemplatePackageSetting.swift in Sources */, 3546355D2C391F38001D7E85 /* PromotionalOfferViewModel.swift in Sources */, 35F249CA2C493D970058993A /* LoadPromotionalOfferUseCase.swift in Sources */, From 5be6750fa5b05170dca08a453692db17ef4e4d85 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:19:59 +0200 Subject: [PATCH 39/90] Fixes some warnings and adds the compiler flag to SemanticVersion[Tests]. --- RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift | 4 ++++ .../CustomerCenter/CustomerCenterViewModelTests.swift | 2 +- Tests/RevenueCatUITests/Data/SemanticVersionTests.swift | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift index 5cef4eb1c1..0d824b936e 100644 --- a/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift +++ b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift @@ -11,6 +11,8 @@ // // Created by JayShortway on 08/08/2024. +#if CUSTOMER_CENTER_ENABLED + import Foundation struct SemanticVersion: Comparable { @@ -67,3 +69,5 @@ enum SemanticVersionError: LocalizedError { } } } + +#endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index caaa4b4f57..22dd54b0ed 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -159,7 +159,7 @@ class CustomerCenterViewModelTests: TestCase { (currentVersion: nil, latestVersion: "1.2.3", expectedAppIsLatestVersion: true) ] for (currentVersion, latestVersion, expectedAppIsLatestVersion) in testCases { - XCTContext.runActivity( + _ = XCTContext.runActivity( named: "Current version = \(currentVersion), " + "latest version = \(latestVersion), " + "expectedAppIsLatestVersion = \(expectedAppIsLatestVersion)" diff --git a/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift index b29ba9892f..0979e2c712 100644 --- a/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift +++ b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift @@ -11,6 +11,8 @@ // // Created by JayShortway on 09/08/2024. +#if CUSTOMER_CENTER_ENABLED + import Nimble @testable import RevenueCatUI import XCTest @@ -59,7 +61,7 @@ class SemanticVersionTests: TestCase { "1.text.whoa" ] for (string) in testCases { - XCTContext.runActivity( + _ = XCTContext.runActivity( named: "Should fail to parse version: '\(string)'" ) { _ in expect { try SemanticVersion(string) } @@ -69,3 +71,5 @@ class SemanticVersionTests: TestCase { } } + +#endif From 305d28e0b8c4214237768dabe0ae8b546efcbe0f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:30:30 +0200 Subject: [PATCH 40/90] Fixes a compile error. --- .../CustomerCenter/Data/CustomerCenterConfigTestData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 1a2b07e357..df922b2532 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -22,7 +22,7 @@ enum CustomerCenterConfigTestData { @available(iOS 14.0, *) // swiftlint:disable:next function_body_length - static func customerCenterData(lastPublishedAppVersion: String) -> CustomerCenterConfigData { + static func customerCenterData(lastPublishedAppVersion: String?) -> CustomerCenterConfigData { CustomerCenterConfigData( screens: [.management: .init( From b9e241b226e53c2067c82022f2522db839f64b46 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:49:59 +0200 Subject: [PATCH 41/90] Fixes some warnings. --- .../CustomerCenter/CustomerCenterViewModelTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 22dd54b0ed..d8d3dbefea 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -159,9 +159,9 @@ class CustomerCenterViewModelTests: TestCase { (currentVersion: nil, latestVersion: "1.2.3", expectedAppIsLatestVersion: true) ] for (currentVersion, latestVersion, expectedAppIsLatestVersion) in testCases { - _ = XCTContext.runActivity( - named: "Current version = \(currentVersion), " + - "latest version = \(latestVersion), " + + XCTContext.runActivity( + named: "Current version = \(currentVersion as Optional), " + + "latest version = \(latestVersion as Optional), " + "expectedAppIsLatestVersion = \(expectedAppIsLatestVersion)" ) { _ in let viewModel = CustomerCenterViewModel( From 50d710289b757b1a9c197f890d2aa26061086e32 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:03:31 +0200 Subject: [PATCH 42/90] SemanticVersion only accepts UInts now. --- .../CustomerCenter/Data/SemanticVersion.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift index 0d824b936e..4bf6a8f435 100644 --- a/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift +++ b/RevenueCatUI/CustomerCenter/Data/SemanticVersion.swift @@ -16,11 +16,11 @@ import Foundation struct SemanticVersion: Comparable { - let major: Int - let minor: Int - let patch: Int + let major: UInt + let minor: UInt + let patch: UInt - init(major: Int, minor: Int, patch: Int) { + init(major: UInt, minor: UInt, patch: UInt) { self.major = major self.minor = minor self.patch = patch @@ -33,12 +33,12 @@ struct SemanticVersion: Comparable { throw SemanticVersionError.invalidVersionString(version) } - let major = Int(version[Range(match.range(at: 1), in: version)!])! + let major = UInt(version[Range(match.range(at: 1), in: version)!])! let minor = match.range(at: 2).location != NSNotFound ? - Int(version[Range(match.range(at: 2), in: version)!])! : + UInt(version[Range(match.range(at: 2), in: version)!])! : 0 let patch = match.range(at: 3).location != NSNotFound ? - Int(version[Range(match.range(at: 3), in: version)!])! : + UInt(version[Range(match.range(at: 3), in: version)!])! : 0 self.init(major: major, minor: minor, patch: patch) From 54c6ff6b55802eeca4f04269d56ef26433eef8f0 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:12:36 +0200 Subject: [PATCH 43/90] appIsLatestVersion is initialized with defaultAppIsLatestVersion. --- .../ViewModels/CustomerCenterViewModel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 81278dab25..3cf664b3d6 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -20,6 +20,9 @@ import RevenueCat #if os(iOS) +// We fail open. +private let defaultAppIsLatestVersion = true + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -36,7 +39,7 @@ import RevenueCat @Published private(set) var subscriptionsAreFromApple: Bool = false @Published - private(set) var appIsLatestVersion: Bool = false + private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion // @PublicForExternalTesting @Published @@ -50,9 +53,6 @@ import RevenueCat @Published var configuration: CustomerCenterConfigData? { didSet { - // We fail open. - let defaultAppIsLatestVersion = true - guard let currentVersionString = currentAppVersion?.takeVersion() else { self.appIsLatestVersion = defaultAppIsLatestVersion return From 8072747c361a86ca14905fe499f484aee73641a9 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:01:31 +0200 Subject: [PATCH 44/90] Renames takeVersion to versionString. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 3cf664b3d6..4135d887a7 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -53,11 +53,11 @@ private let defaultAppIsLatestVersion = true @Published var configuration: CustomerCenterConfigData? { didSet { - guard let currentVersionString = currentAppVersion?.takeVersion() else { + guard let currentVersionString = currentAppVersion?.versionString() else { self.appIsLatestVersion = defaultAppIsLatestVersion return } - guard let latestVersionString = configuration?.lastPublishedAppVersion?.takeVersion() else { + guard let latestVersionString = configuration?.lastPublishedAppVersion?.versionString() else { self.appIsLatestVersion = defaultAppIsLatestVersion return } @@ -157,7 +157,7 @@ private let defaultAppIsLatestVersion = true fileprivate extension String { /// Takes the first characters of this string, if they conform to Major.Minor.Patch. Returns nil otherwise. /// Note that Minor and Patch are optional. So if this string starts with a single number, that number is returned. - func takeVersion() -> String? { + func versionString() -> String? { do { let pattern = #"^(\d+)(?:\.\d+)?(?:\.\d+)?"# let regex = try NSRegularExpression(pattern: pattern) From a64e6b44a13697bac8422ef2841d34bec1c35c97 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:25:10 +0200 Subject: [PATCH 45/90] Fixes a compilation error in SemanticVersionTests. --- Tests/RevenueCatUITests/Data/SemanticVersionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift index 0979e2c712..9252ed1d7d 100644 --- a/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift +++ b/Tests/RevenueCatUITests/Data/SemanticVersionTests.swift @@ -20,7 +20,7 @@ import XCTest class SemanticVersionTests: TestCase { func testValidVersionString() { - let testCases = [ + let testCases: [(string: String, major: UInt, minor: UInt, patch: UInt)] = [ (string: "12.4503.2", major: 12, minor: 4503, patch: 2), (string: "12.4503", major: 12, minor: 4503, patch: 0), (string: "12", major: 12, minor: 0, patch: 0), From 385ab4e1cc0509210de2bdb9f39ae4a32df09627 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:34:13 +0200 Subject: [PATCH 46/90] defaultAppIsLatestVersion is static now. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 4135d887a7..31b2a854ee 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -20,14 +20,13 @@ import RevenueCat #if os(iOS) -// We fail open. -private let defaultAppIsLatestVersion = true - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) @MainActor class CustomerCenterViewModel: ObservableObject { + // We fail open. + private static let defaultAppIsLatestVersion = true typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo typealias CurrentVersionFetcher = () -> String? From bbe3fca8961bc14661d6a83c50fb091f13fbf395 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 15:27:45 +0200 Subject: [PATCH 47/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 + .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2c9cb2513e..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: []), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index d64dec5895..5b6f7da3fe 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6790,6 +6790,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From 7329cb65bba9150fb56f89f5f842ac46b5057a7b Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:48:58 +0200 Subject: [PATCH 48/90] Uses the existing SystemInfo.appVersion. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 31b2a854ee..1a70dcb531 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -88,7 +88,7 @@ import RevenueCat return try await Purchases.shared.customerInfo() }, currentVersionFetcher: @escaping CurrentVersionFetcher = { - Bundle.main.infoDictionary?["CFBundleVersion"] as? String + SystemInfo.appVersion } ) { self.state = .notLoaded From 19b78817d6675482ef93a1a8b48a0c6874985a53 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:56:07 +0200 Subject: [PATCH 49/90] Uses a single guard let statement. --- .../ViewModels/CustomerCenterViewModel.swift | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 1a70dcb531..383752509b 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -52,22 +52,17 @@ import RevenueCat @Published var configuration: CustomerCenterConfigData? { didSet { - guard let currentVersionString = currentAppVersion?.versionString() else { - self.appIsLatestVersion = defaultAppIsLatestVersion - return - } - guard let latestVersionString = configuration?.lastPublishedAppVersion?.versionString() else { - self.appIsLatestVersion = defaultAppIsLatestVersion + guard + let currentVersionString = currentAppVersion?.versionString(), + let latestVersionString = configuration?.lastPublishedAppVersion?.versionString(), + let currentVersion = try? SemanticVersion(currentVersionString), + let latestVersion = try? SemanticVersion(latestVersionString) + else { + self.appIsLatestVersion = Self.defaultAppIsLatestVersion return } - do { - let currentVersion = try SemanticVersion(currentVersionString) - let latestVersion = try SemanticVersion(latestVersionString) - self.appIsLatestVersion = currentVersion >= latestVersion - } catch { - self.appIsLatestVersion = defaultAppIsLatestVersion - } + self.appIsLatestVersion = currentVersion >= latestVersion } } From 14732226881de97bc2a7e108e5d5666cb2d7fa3f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:58:16 +0200 Subject: [PATCH 50/90] Adds some more test cases. --- .../CustomerCenter/CustomerCenterViewModelTests.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index d8d3dbefea..60ac2592d2 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -154,9 +154,14 @@ class CustomerCenterViewModelTests: TestCase { (currentVersion: "not-a-number", latestVersion: "not-a-number-either", expectedAppIsLatestVersion: true), (currentVersion: "not-a-number", latestVersion: "1.2.3", expectedAppIsLatestVersion: true), (currentVersion: "1.2.3", latestVersion: "not-a-number", expectedAppIsLatestVersion: true), + (currentVersion: "not.a.number", latestVersion: "1.2.3", expectedAppIsLatestVersion: true), + (currentVersion: "1.2.3", latestVersion: "not.a.number", expectedAppIsLatestVersion: true), (currentVersion: nil, latestVersion: nil, expectedAppIsLatestVersion: true), (currentVersion: "1.2.3", latestVersion: nil, expectedAppIsLatestVersion: true), - (currentVersion: nil, latestVersion: "1.2.3", expectedAppIsLatestVersion: true) + (currentVersion: nil, latestVersion: "1.2.3", expectedAppIsLatestVersion: true), + (currentVersion: "", latestVersion: "", expectedAppIsLatestVersion: true), + (currentVersion: "1.2.3", latestVersion: "", expectedAppIsLatestVersion: true), + (currentVersion: "", latestVersion: "1.2.3", expectedAppIsLatestVersion: true) ] for (currentVersion, latestVersion, expectedAppIsLatestVersion) in testCases { XCTContext.runActivity( From 1ae81ea338847d0be2e1286d1487d038a12fc036 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:58:39 +0200 Subject: [PATCH 51/90] Revert "Revert "Disable `CustomerCenter` build flags (#4160)"" This reverts commit bbe3fca8961bc14661d6a83c50fb091f13fbf395. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 - .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8feea04c3d..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), + swiftSettings: []), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 5b6f7da3fe..d64dec5895 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6790,7 +6790,6 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From d32e24a9be976642b2573e14568e360565ac6611 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:02:47 +0200 Subject: [PATCH 52/90] Uses CFBundleShortVersionString directly. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 383752509b..96c87d51bc 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -83,7 +83,7 @@ import RevenueCat return try await Purchases.shared.customerInfo() }, currentVersionFetcher: @escaping CurrentVersionFetcher = { - SystemInfo.appVersion + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } ) { self.state = .notLoaded From b72d05cfcfed2358dd594a682d8bc5d6d10103f3 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:42:20 +0200 Subject: [PATCH 53/90] Fixes CustomerCenterConfigDataAPI. --- .../SwiftAPITester/CustomerCenterConfigDataAPI.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerCenterConfigDataAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerCenterConfigDataAPI.swift index 4b2414e02d..45425380ea 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerCenterConfigDataAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerCenterConfigDataAPI.swift @@ -15,11 +15,13 @@ func checkCustomerCenterConfigData(_ data: CustomerCenterConfigData) { let appearance: CustomerCenterConfigData.Appearance = data.appearance let localization: CustomerCenterConfigData.Localization = data.localization let support: CustomerCenterConfigData.Support = data.support + let lastPublishedAppVersion = data.lastPublishedAppVersion let _: CustomerCenterConfigData = .init(screens: screens, appearance: appearance, localization: localization, - support: support) + support: support, + lastPublishedAppVersion: lastPublishedAppVersion) } func checkHelpPath(_ path: CustomerCenterConfigData.HelpPath) { From aae889a070f6751acf4578677d5a6025124b8116 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 15:27:45 +0200 Subject: [PATCH 54/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 + .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2c9cb2513e..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: []), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index d64dec5895..5b6f7da3fe 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6790,6 +6790,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From 1412b6b61f2ce6630a1772effbb8f498a2150016 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:57:58 +0200 Subject: [PATCH 55/90] Revert "Revert "Disable `CustomerCenter` build flags (#4160)"" This reverts commit aae889a070f6751acf4578677d5a6025124b8116. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 - .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8feea04c3d..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), + swiftSettings: []), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 5b6f7da3fe..d64dec5895 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6790,7 +6790,6 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From a9d86aeb82c74bd66de7bacbeb2f28d5e0c9cd3f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:55:21 +0200 Subject: [PATCH 56/90] Removes references to non-existent ManageSubscriptionsButtonStyle.swift file. --- RevenueCat.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index bb89344ef7..312a7fb4d0 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -196,7 +196,6 @@ 3537566D2C382C2800A1B8D6 /* NoSubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */; }; 3537566E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */; }; 3537566F2C382C2800A1B8D6 /* WrongPlatformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */; }; - 353756702C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */; }; 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */; }; 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353756632C382C2800A1B8D6 /* URLUtilities.swift */; }; 3543913626F90D6A00E669DF /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */; }; @@ -1366,7 +1365,6 @@ 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoSubscriptionsView.swift; sourceTree = ""; }; 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePurchasesAlert.swift; sourceTree = ""; }; 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WrongPlatformView.swift; sourceTree = ""; }; - 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsButtonStyle.swift; sourceTree = ""; }; 353756622C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsPurchaseType.swift; sourceTree = ""; }; 353756632C382C2800A1B8D6 /* URLUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLUtilities.swift; sourceTree = ""; }; 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = ""; }; @@ -3161,7 +3159,6 @@ 353756562C382C2800A1B8D6 /* Data */, 3537565A2C382C2800A1B8D6 /* ViewModels */, 353756602C382C2800A1B8D6 /* Views */, - 353756612C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift */, 353756632C382C2800A1B8D6 /* URLUtilities.swift */, 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */, 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */, @@ -5886,7 +5883,6 @@ 887A606B2C1D037000E1A461 /* TrialOrIntroEligibilityChecker+TestData.swift in Sources */, 887A606C2C1D037000E1A461 /* Constants.swift in Sources */, 887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */, - 353756702C382C2800A1B8D6 /* ManageSubscriptionsButtonStyle.swift in Sources */, 887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */, 3537566A2C382C2800A1B8D6 /* ManageSubscriptionsViewModel.swift in Sources */, 887A60842C1D037000E1A461 /* ConsistentPackageContentView.swift in Sources */, From e1854c0dd17b47f95cc9f7416aefeb35adea6808 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 15:27:45 +0200 Subject: [PATCH 57/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 + .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2c9cb2513e..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: []), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 312a7fb4d0..7871eaa17b 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6796,6 +6796,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From d87b22f272f1331da9424475baa019ead29fe232 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:20:55 +0200 Subject: [PATCH 58/90] Adds references to ButtonStyles.swift. --- RevenueCat.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 7871eaa17b..c3546232e1 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -663,6 +663,7 @@ 57FFD2512922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; }; 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 */; }; 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */; }; 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; @@ -1792,6 +1793,7 @@ 57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxEnvironmentDetectorTests.swift; sourceTree = ""; }; 57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSandboxEnvironmentDetector.swift; sourceTree = ""; }; 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; + 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; @@ -3160,6 +3162,7 @@ 3537565A2C382C2800A1B8D6 /* ViewModels */, 353756602C382C2800A1B8D6 /* Views */, 353756632C382C2800A1B8D6 /* URLUtilities.swift */, + 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */, 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */, 3511088E2C47F6DA0048C4D8 /* CustomerInfo+CurrentEntitlement.swift */, 357CEC6F2C5940CE00A80837 /* ColorFromAppearance.swift */, @@ -5818,6 +5821,7 @@ 2D2AFE8D2C6A834D00D1B0B4 /* TestData.swift in Sources */, 887A60C92C1D037000E1A461 /* PurchaseButton.swift in Sources */, 2D2AFE912C6A9EF500D1B0B4 /* Binding+Extensions.swift in Sources */, + 7706ED3E2C6E374D0004B9F9 /* ButtonStyles.swift in Sources */, 887A60812C1D037000E1A461 /* PaywallData+Default.swift in Sources */, 887A606A2C1D037000E1A461 /* TrialOrIntroEligibilityChecker.swift in Sources */, 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */, From 469bc71d4a6582f1abd67b4b291e44e79125d75d Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 15:27:45 +0200 Subject: [PATCH 59/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- RevenueCat.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index c3546232e1..3d98978330 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6469,7 +6469,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xros*]" = "$(inherited) VISION_OS"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xrsimulator*]" = "$(inherited) VISION_OS"; TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; From 7402a25ee2b40fded52b4a71308565decfc8576a Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:21:46 +0200 Subject: [PATCH 60/90] Adds AppUpdateWarningView. --- RevenueCat.xcodeproj/project.pbxproj | 4 + .../Views/AppUpdateWarningView.swift | 116 ++++++++++++++++++ .../CustomerCenterConfigData.swift | 17 ++- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 456ab65038..b261502631 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -664,6 +664,7 @@ 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 */; }; + 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 */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; @@ -1794,6 +1795,7 @@ 57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSandboxEnvironmentDetector.swift; sourceTree = ""; }; 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; + 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateWarningView.swift; sourceTree = ""; }; 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; @@ -3149,6 +3151,7 @@ 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */, 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */, + 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */, ); path = Views; sourceTree = ""; @@ -5802,6 +5805,7 @@ 887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */, 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */, 887A60C72C1D037000E1A461 /* PackageButtonStyle.swift in Sources */, + 77372D992C6F8C7B008E59D3 /* AppUpdateWarningView.swift in Sources */, 887A60C52C1D037000E1A461 /* IntroEligibilityStateView.swift in Sources */, 88A543E72C37A4C40039C6A5 /* TierSelectorView.swift in Sources */, 88A543E12C37A4820039C6A5 /* TemplateView+MultiTier.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift new file mode 100644 index 0000000000..368dd5d6cc --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -0,0 +1,116 @@ +// +// 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 +// +// AppUpdateWarningView.swift +// +// Created by JayShortway on 16/08/2024. + +#if CUSTOMER_CENTER_ENABLED + +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct AppUpdateWarningView: View { + let onUpdateAppClick: () -> Void + let onContinueAnywayClick: () -> Void + + @Environment(\.dismiss) + var dismiss + + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + @Environment(\.appearance) + private var appearance: CustomerCenterConfigData.Appearance + @Environment(\.colorScheme) + private var colorScheme + + @ViewBuilder + var content: some View { + ZStack { + if let background = Color.from(colorInformation: appearance.backgroundColor, for: colorScheme) { + background.edgesIgnoringSafeArea(.all) + } + let textColor = Color.from(colorInformation: appearance.textColor, for: colorScheme) + + VStack { + CompatibilityContentUnavailableView( + title: localization.commonLocalizedString(for: .updateWarningTitle), + description: localization.commonLocalizedString(for: .updateWarningDescription), + systemImage: "arrowshape.up.circle.fill" + ) + + Button(localization.commonLocalizedString(for: .updateWarningUpdate)) { + onUpdateAppClick() + } + .buttonStyle(ProminentButtonStyle()) + .padding(.bottom) + + Button(localization.commonLocalizedString(for: .updateWarningIgnore)) { + onContinueAnywayClick() + } + } + .padding(.horizontal) + .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + DismissCircleButton { + dismiss() + } + } + } + } + + var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + content + } + } else { + NavigationView { + content + } + } + } +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct AppUpdateWarningView_Previews: PreviewProvider { + + static var previews: some View { + Group { + AppUpdateWarningView( + onUpdateAppClick: { + + }, + onContinueAnywayClick: { + + } + ) + } + } + +} + +#endif + +#endif + +#endif diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 4844b77653..1049ac9505 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -17,7 +17,7 @@ import Foundation -// swiftlint:disable missing_docs nesting file_length +// swiftlint:disable missing_docs nesting file_length type_body_length public typealias RCColor = PaywallColor public struct CustomerCenterConfigData { @@ -73,6 +73,10 @@ public struct CustomerCenterConfigData { case defaultBody = "default_body" case defaultSubject = "default_subject" case dismiss = "dismiss" + case updateWarningTitle = "update_warning_title" + case updateWarningDescription = "update_warning_description" + case updateWarningUpdate = "update_warning_update" + case updateWarningIgnore = "update_warning_ignore" var defaultValue: String { switch self { @@ -118,6 +122,17 @@ public struct CustomerCenterConfigData { return "Support Request" case .dismiss: return "Dismiss" + case .updateWarningTitle: + return "Update the app to fix your problem" + case .updateWarningDescription: + return """ + A new update is available. Downloading the latest version of the app can help you solve your \ + problem. + """ + case .updateWarningUpdate: + return "Update" + case .updateWarningIgnore: + return "Continue" } } From 9e38a87c0622c8f0063b66760eaed5f0bf8a846b Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:26:06 +0200 Subject: [PATCH 61/90] CustomerCenterView shows AppUpdateWarningView when needed. --- .../ViewModels/CustomerCenterViewModel.swift | 5 +++++ .../CustomerCenter/Views/CustomerCenterView.swift | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 96c87d51bc..5aa3c70754 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -146,6 +146,11 @@ import RevenueCat return .purchasesNotFound } } + + func onAppUpdateClick() { + // swiftlint:disable:next todo + // TODO: implement opening the App Store + } } fileprivate extension String { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 80f89fcf75..f73c61b0e5 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -28,6 +28,7 @@ import SwiftUI public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel + @State private var ignoreAppUpdateWarning: Bool = false private var localization: CustomerCenterConfigData.Localization private var appearance: CustomerCenterConfigData.Appearance @@ -91,8 +92,15 @@ private extension CustomerCenterView { if viewModel.hasSubscriptions { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { - ManageSubscriptionsView(screen: screen, - customerCenterActionHandler: viewModel.customerCenterActionHandler) + if ignoreAppUpdateWarning || viewModel.appIsLatestVersion { + ManageSubscriptionsView(screen: screen, + customerCenterActionHandler: viewModel.customerCenterActionHandler) + } else { + AppUpdateWarningView( + onUpdateAppClick: { viewModel.onAppUpdateClick() }, + onContinueAnywayClick: { ignoreAppUpdateWarning = true } + ) + } } else { WrongPlatformView() } From a4519d9ae2792e6b9c6560fba630e6dbd7fa987d Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:40:48 +0200 Subject: [PATCH 62/90] Revert "Revert "Disable `CustomerCenter` build flags (#4160)"" This reverts commit 469bc71d4a6582f1abd67b4b291e44e79125d75d. --- RevenueCat.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index b261502631..72a9030220 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6475,7 +6475,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xros*]" = "$(inherited) VISION_OS"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xrsimulator*]" = "$(inherited) VISION_OS"; TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; From ca983e0001664d7e9fff5ae262b100bfb9d0a42d Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:42:06 +0200 Subject: [PATCH 63/90] Revert "Revert "Disable `CustomerCenter` build flags (#4160)"" This reverts commit e1854c0dd17b47f95cc9f7416aefeb35adea6808. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 1 - .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8feea04c3d..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), + swiftSettings: []), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 72a9030220..341799a7f0 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6824,7 +6824,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = NO; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From a1ff1ad6e8747555f29e135cdb9d909b5d5b9409 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 9 Aug 2024 15:27:45 +0200 Subject: [PATCH 64/90] Revert "Disable `CustomerCenter` build flags (#4160)" This reverts commit 92841076720430ec712fcd9c7f547007f024bd7a. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 3 ++- .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 2c9cb2513e..8feea04c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [visionOSSetting]), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: []), + swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 341799a7f0..b261502631 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6475,7 +6475,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xros*]" = "$(inherited) VISION_OS"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xrsimulator*]" = "$(inherited) VISION_OS"; TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; @@ -6824,6 +6824,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = NO; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 032adc7703..4d458e695f 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From b026b3a3a4c2ce589fb4c3c0cb40d0bfd4029d2c Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:07:31 +0200 Subject: [PATCH 65/90] Adds OpenAppStoreViewController. --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../Views/OpenAppStoreViewController.swift | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index b261502631..c15db601d6 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -665,6 +665,7 @@ 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; }; 7706ED3E2C6E374D0004B9F9 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */; }; 77372D992C6F8C7B008E59D3 /* AppUpdateWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */; }; + 77372DC52C739313008E59D3 /* OpenAppStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */; }; 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */; }; 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; @@ -1796,6 +1797,7 @@ 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateWarningView.swift; sourceTree = ""; }; + 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAppStoreViewController.swift; sourceTree = ""; }; 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; @@ -3152,6 +3154,7 @@ 3537565F2C382C2800A1B8D6 /* WrongPlatformView.swift */, 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */, 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */, + 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */, ); path = Views; sourceTree = ""; @@ -5855,6 +5858,7 @@ 887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */, 35F249CE2C493E3D0058993A /* CustomerCenterPurchases.swift in Sources */, 353756672C382C2800A1B8D6 /* SubscriptionInformation.swift in Sources */, + 77372DC52C739313008E59D3 /* OpenAppStoreViewController.swift in Sources */, 887A60682C1D037000E1A461 /* TemplateError.swift in Sources */, 887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */, 887A60742C1D037000E1A461 /* Strings.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift new file mode 100644 index 0000000000..f9be1dd4e1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift @@ -0,0 +1,62 @@ +// +// 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 +// +// OpenAppStoreViewController.swift +// +// Created by JayShortway on 19/08/2024. + +import Foundation +import StoreKit +import UIKit + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +class OpenAppStoreViewController: UIViewController, SKStoreProductViewControllerDelegate { + let onDismiss: (SKStoreProductViewController) -> Void + + init( + onDismiss: @escaping (SKStoreProductViewController) -> Void = { viewController in + viewController.dismiss(animated: true) + } + ) { + self.onDismiss = onDismiss + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Opens the App Store for the provided productId in-app using SKStoreProductViewController. Falls back to + /// redirecting to the App Store if this fails. + func openAppStore(productId: UInt) { + let storeProductViewController = SKStoreProductViewController() + storeProductViewController.delegate = self + let parameters = [SKStoreProductParameterITunesItemIdentifier: productId] + + storeProductViewController.loadProduct(withParameters: parameters) { _, error in + guard error == nil, + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController else { + + let appStoreUrl = URL(string: "https://itunes.apple.com/app/id\(productId)")! + UIApplication.shared.open(appStoreUrl) + return + } + + rootViewController.present(storeProductViewController, animated: true, completion: nil) + } + } + + func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + onDismiss(viewController) + } +} From 3df8a5b0563771cfafebe34039156c98845f35a7 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:14:07 +0200 Subject: [PATCH 66/90] Adds CUSTOMER_CENTER_ENABLED flag to OpenAppStoreViewController. --- .../CustomerCenter/Views/OpenAppStoreViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift index f9be1dd4e1..c0e96365cb 100644 --- a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift +++ b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift @@ -11,6 +11,8 @@ // // Created by JayShortway on 19/08/2024. +#if CUSTOMER_CENTER_ENABLED + import Foundation import StoreKit import UIKit @@ -60,3 +62,5 @@ class OpenAppStoreViewController: UIViewController, SKStoreProductViewController onDismiss(viewController) } } + +#endif From d81da1d77d0b284c76b7b1df08e47b326f2b692e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:15:10 +0200 Subject: [PATCH 67/90] Adds an initializer to AppUpdateWarningView which automatically opens the App Store for the provided productId in onUpdateAppClick. --- .../CustomerCenter/Views/AppUpdateWarningView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 368dd5d6cc..219335c1e5 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -26,6 +26,18 @@ struct AppUpdateWarningView: View { let onUpdateAppClick: () -> Void let onContinueAnywayClick: () -> Void + init(onUpdateAppClick: @escaping () -> Void, onContinueAnywayClick: @escaping () -> Void) { + self.onUpdateAppClick = onUpdateAppClick + self.onContinueAnywayClick = onContinueAnywayClick + } + + init(productId: UInt, onContinueAnywayClick: @escaping () -> Void) { + self.init( + onUpdateAppClick: { OpenAppStoreViewController().openAppStore(productId: productId) }, + onContinueAnywayClick: onContinueAnywayClick + ) + } + @Environment(\.dismiss) var dismiss From cb22b6ef5a5466cabcaefdffa76587394d24e96c Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:17:02 +0200 Subject: [PATCH 68/90] CustomerCenterViewModel no longer handles opening the App Store. --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 5 ----- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 5aa3c70754..96c87d51bc 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -146,11 +146,6 @@ import RevenueCat return .purchasesNotFound } } - - func onAppUpdateClick() { - // swiftlint:disable:next todo - // TODO: implement opening the App Store - } } fileprivate extension String { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index f73c61b0e5..6d3ef0a6b4 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -97,7 +97,8 @@ private extension CustomerCenterView { customerCenterActionHandler: viewModel.customerCenterActionHandler) } else { AppUpdateWarningView( - onUpdateAppClick: { viewModel.onAppUpdateClick() }, + // TODO: Get productId from config. + productId: 545551605, onContinueAnywayClick: { ignoreAppUpdateWarning = true } ) } From 300a2645c34bb867ada5613613c9c4d67fb14827 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:26:08 +0200 Subject: [PATCH 69/90] Adds productId fields to CustomerCenterConfigData and CustomerCenterConfigResponse. --- .../CustomerCenter/Data/CustomerCenterConfigTestData.swift | 3 ++- Sources/CustomerCenter/CustomerCenterConfigData.swift | 6 +++++- .../Networking/Responses/CustomerCenterConfigResponse.swift | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index df922b2532..5e68e76b3d 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -103,7 +103,8 @@ enum CustomerCenterConfigTestData { ] ), support: .init(email: "test-support@revenuecat.com"), - lastPublishedAppVersion: lastPublishedAppVersion + lastPublishedAppVersion: lastPublishedAppVersion, + productId: 1 ) } diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 1049ac9505..91cf0bb430 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -27,17 +27,20 @@ public struct CustomerCenterConfigData { public let localization: Localization public let support: Support public let lastPublishedAppVersion: String? + public let productId: UInt public init(screens: [Screen.ScreenType: Screen], appearance: Appearance, localization: Localization, support: Support, - lastPublishedAppVersion: String?) { + lastPublishedAppVersion: String?, + productId: UInt) { self.screens = screens self.appearance = appearance self.localization = localization self.support = support self.lastPublishedAppVersion = lastPublishedAppVersion + self.productId = productId } public struct Localization { @@ -348,6 +351,7 @@ extension CustomerCenterConfigData { }) self.support = Support(from: response.customerCenter.support) self.lastPublishedAppVersion = response.lastPublishedAppVersion + self.productId = response.productId } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 6690badaac..3798fe1511 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -21,6 +21,7 @@ struct CustomerCenterConfigResponse { let customerCenter: CustomerCenter let lastPublishedAppVersion: String? + let productId: UInt struct CustomerCenter { From 310932e28b63327cf5e9a7bb5e2b4de3e3bd2c1d Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:27:05 +0200 Subject: [PATCH 70/90] CustomerCenterView provides the productId from CustomerCenterConfigurationData to AppUpdateWarningView. --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 6d3ef0a6b4..433f6e07a6 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -97,8 +97,7 @@ private extension CustomerCenterView { customerCenterActionHandler: viewModel.customerCenterActionHandler) } else { AppUpdateWarningView( - // TODO: Get productId from config. - productId: 545551605, + productId: configuration.productId, onContinueAnywayClick: { ignoreAppUpdateWarning = true } ) } From 88ca98d5bc91ea8abc7c9e850897001a6921eb4e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:28:29 +0200 Subject: [PATCH 71/90] Revert "Revert "Disable `CustomerCenter` build flags (#4160)"" This reverts commit a1ff1ad6e8747555f29e135cdb9d909b5d5b9409. --- Package.swift | 4 ++-- RevenueCat.xcodeproj/project.pbxproj | 3 +-- .../PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 8feea04c3d..2c9cb2513e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,7 @@ let package = Package( resources: [ .copy("../Sources/PrivacyInfo.xcprivacy") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED"), visionOSSetting]), + swiftSettings: [visionOSSetting]), .target(name: "RevenueCat_CustomEntitlementComputation", path: "CustomEntitlementComputation", exclude: ["Info.plist", "LocalReceiptParsing/ReceiptParser-only-files"], @@ -76,7 +76,7 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ], - swiftSettings: [.define("CUSTOMER_CENTER_ENABLED")]), + swiftSettings: []), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 67d0c3edfa..18725f3626 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -6479,7 +6479,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xros*]" = "$(inherited) VISION_OS"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xrsimulator*]" = "$(inherited) VISION_OS"; TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; @@ -6828,7 +6828,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = NO; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CUSTOMER_CENTER_ENABLED; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 4d458e695f..032adc7703 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CUSTOMER_CENTER_ENABLED"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; From e617f114d5df6f7fa5c58f4ddd73880a96c9145e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:26:46 +0200 Subject: [PATCH 72/90] Add a descriptive comment. --- .../CustomerCenter/Views/OpenAppStoreViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift index c0e96365cb..39434d5363 100644 --- a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift +++ b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift @@ -48,7 +48,7 @@ class OpenAppStoreViewController: UIViewController, SKStoreProductViewController guard error == nil, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController else { - + // productId is a positive integer, so it is safe to construct a URL from it. let appStoreUrl = URL(string: "https://itunes.apple.com/app/id\(productId)")! UIApplication.shared.open(appStoreUrl) return From 595a4b210ddf1df04c6ea233fd11d488973402c2 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:07:53 +0200 Subject: [PATCH 73/90] Correctly constructs CompatibilityContentUnavailableView using updated initializer. --- .../CustomerCenter/Views/AppUpdateWarningView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 368dd5d6cc..ef5ac4aedc 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -46,9 +46,9 @@ struct AppUpdateWarningView: View { VStack { CompatibilityContentUnavailableView( - title: localization.commonLocalizedString(for: .updateWarningTitle), - description: localization.commonLocalizedString(for: .updateWarningDescription), - systemImage: "arrowshape.up.circle.fill" + localization.commonLocalizedString(for: .updateWarningTitle), + systemImage: "arrowshape.up.circle.fill", + description: Text(localization.commonLocalizedString(for: .updateWarningDescription)) ) Button(localization.commonLocalizedString(for: .updateWarningUpdate)) { From 612ec1920728a31dfc26faa39cb12936481adf18 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:49:18 +0200 Subject: [PATCH 74/90] Apply copy suggestions from code review Co-authored-by: James Borthwick <109382862+jamesrb1@users.noreply.github.com> --- Sources/CustomerCenter/CustomerCenterConfigData.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 1049ac9505..016361cfe8 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -123,10 +123,10 @@ public struct CustomerCenterConfigData { case .dismiss: return "Dismiss" case .updateWarningTitle: - return "Update the app to fix your problem" + return "Update the app to fix the problem." case .updateWarningDescription: return """ - A new update is available. Downloading the latest version of the app can help you solve your \ + A new update is available. Downloading the latest version of the app may help solve the \ problem. """ case .updateWarningUpdate: From e86ca2a358273ddb8c406681932bc7a967b7cf0c Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:33:23 +0200 Subject: [PATCH 75/90] Uses .compatibleTopBarTrailing. --- RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index ef5ac4aedc..30c2e549a7 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -65,7 +65,7 @@ struct AppUpdateWarningView: View { .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .compatibleTopBarTrailing) { DismissCircleButton { dismiss() } From eac6e3e00c9e465b29f2b4533011f1ba390ef834 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:40:49 +0200 Subject: [PATCH 76/90] Adds an animation when navigating past the app update warning. --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index f73c61b0e5..c073bfa946 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -98,7 +98,11 @@ private extension CustomerCenterView { } else { AppUpdateWarningView( onUpdateAppClick: { viewModel.onAppUpdateClick() }, - onContinueAnywayClick: { ignoreAppUpdateWarning = true } + onContinueAnywayClick: { + withAnimation { + ignoreAppUpdateWarning = true + } + } ) } } else { From bd1126013da54ca917574e7f6cca5ad4833722a6 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:25:04 +0200 Subject: [PATCH 77/90] Ensures SKStoreProductParameterITunesItemIdentifier is an NSNumber. --- .../CustomerCenter/Views/OpenAppStoreViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift index 39434d5363..8beaa16ff4 100644 --- a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift +++ b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift @@ -42,7 +42,7 @@ class OpenAppStoreViewController: UIViewController, SKStoreProductViewController func openAppStore(productId: UInt) { let storeProductViewController = SKStoreProductViewController() storeProductViewController.delegate = self - let parameters = [SKStoreProductParameterITunesItemIdentifier: productId] + let parameters = [SKStoreProductParameterITunesItemIdentifier: NSNumber(value: productId)] storeProductViewController.loadProduct(withParameters: parameters) { _, error in guard error == nil, From 0f34733d8e221669d4134ba36d24dec4d1c0cb06 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:46:39 +0200 Subject: [PATCH 78/90] AppUpdateWarningView opens the App Store directly. --- RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 553b62fe75..d13c1e83bf 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -33,7 +33,10 @@ struct AppUpdateWarningView: View { init(productId: UInt, onContinueAnywayClick: @escaping () -> Void) { self.init( - onUpdateAppClick: { OpenAppStoreViewController().openAppStore(productId: productId) }, + onUpdateAppClick: { + // productId is a positive integer, so it is safe to construct a URL from it. + UIApplication.shared.open(URL(string: "https://itunes.apple.com/app/id\(productId)")!) + }, onContinueAnywayClick: onContinueAnywayClick ) } From b8789c44f4e541b30ba3a7227f484174c1cf284e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:59:17 +0200 Subject: [PATCH 79/90] Deletes OpenAppStoreViewController. --- RevenueCat.xcodeproj/project.pbxproj | 4 -- .../Views/OpenAppStoreViewController.swift | 66 ------------------- 2 files changed, 70 deletions(-) delete mode 100644 RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 4662abada1..895402338a 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -666,7 +666,6 @@ 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; }; 7706ED3E2C6E374D0004B9F9 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */; }; 77372D992C6F8C7B008E59D3 /* AppUpdateWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */; }; - 77372DC52C739313008E59D3 /* OpenAppStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */; }; 77791ECF2C6B852000BCEF03 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */; }; 777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; @@ -1801,7 +1800,6 @@ 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; 7706ED3D2C6E374D0004B9F9 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateWarningView.swift; sourceTree = ""; }; - 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAppStoreViewController.swift; sourceTree = ""; }; 77791ECE2C6B851F00BCEF03 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; 777FB4872C661C0600CD4749 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; @@ -3163,7 +3161,6 @@ C3AD12B92C6EA61F00A4F86F /* CompatibilityNavigationStack.swift */, C3AD12BB2C6EA69D00A4F86F /* SubscriptionDetailsView.swift */, 77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */, - 77372DC42C739313008E59D3 /* OpenAppStoreViewController.swift */, ); path = Views; sourceTree = ""; @@ -5869,7 +5866,6 @@ 887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */, 35F249CE2C493E3D0058993A /* CustomerCenterPurchases.swift in Sources */, 353756672C382C2800A1B8D6 /* SubscriptionInformation.swift in Sources */, - 77372DC52C739313008E59D3 /* OpenAppStoreViewController.swift in Sources */, 887A60682C1D037000E1A461 /* TemplateError.swift in Sources */, 887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */, C3AD12BC2C6EA69D00A4F86F /* SubscriptionDetailsView.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift b/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift deleted file mode 100644 index 8beaa16ff4..0000000000 --- a/RevenueCatUI/CustomerCenter/Views/OpenAppStoreViewController.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// 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 -// -// OpenAppStoreViewController.swift -// -// Created by JayShortway on 19/08/2024. - -#if CUSTOMER_CENTER_ENABLED - -import Foundation -import StoreKit -import UIKit - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@available(macOS, unavailable) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -class OpenAppStoreViewController: UIViewController, SKStoreProductViewControllerDelegate { - let onDismiss: (SKStoreProductViewController) -> Void - - init( - onDismiss: @escaping (SKStoreProductViewController) -> Void = { viewController in - viewController.dismiss(animated: true) - } - ) { - self.onDismiss = onDismiss - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /// Opens the App Store for the provided productId in-app using SKStoreProductViewController. Falls back to - /// redirecting to the App Store if this fails. - func openAppStore(productId: UInt) { - let storeProductViewController = SKStoreProductViewController() - storeProductViewController.delegate = self - let parameters = [SKStoreProductParameterITunesItemIdentifier: NSNumber(value: productId)] - - storeProductViewController.loadProduct(withParameters: parameters) { _, error in - guard error == nil, - let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController else { - // productId is a positive integer, so it is safe to construct a URL from it. - let appStoreUrl = URL(string: "https://itunes.apple.com/app/id\(productId)")! - UIApplication.shared.open(appStoreUrl) - return - } - - rootViewController.present(storeProductViewController, animated: true, completion: nil) - } - } - - func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { - onDismiss(viewController) - } -} - -#endif From 24e0f0f464b152c193791f0131205213a53face5 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:37:44 +0200 Subject: [PATCH 80/90] Sets the correct field name in CustomerCenterConfigResponse and handles nullabilty. --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 4 ++-- Sources/CustomerCenter/CustomerCenterConfigData.swift | 4 ++-- .../Networking/Responses/CustomerCenterConfigResponse.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 9b7df555e7..77867acade 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -94,12 +94,12 @@ private extension CustomerCenterView { if viewModel.hasSubscriptions { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { - if ignoreAppUpdateWarning || viewModel.appIsLatestVersion { + if ignoreAppUpdateWarning || viewModel.appIsLatestVersion || configuration.productId == nil { ManageSubscriptionsView(screen: screen, customerCenterActionHandler: viewModel.customerCenterActionHandler) } else { AppUpdateWarningView( - productId: configuration.productId, + productId: configuration.productId!, onContinueAnywayClick: { withAnimation { ignoreAppUpdateWarning = true diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 2f4550a4cd..68a146fc9f 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -27,7 +27,7 @@ public struct CustomerCenterConfigData { public let localization: Localization public let support: Support public let lastPublishedAppVersion: String? - public let productId: UInt + public let productId: UInt? public init(screens: [Screen.ScreenType: Screen], appearance: Appearance, @@ -351,7 +351,7 @@ extension CustomerCenterConfigData { }) self.support = Support(from: response.customerCenter.support) self.lastPublishedAppVersion = response.lastPublishedAppVersion - self.productId = response.productId + self.productId = response.itunesTrackId } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 3798fe1511..facfd95ac4 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -21,7 +21,7 @@ struct CustomerCenterConfigResponse { let customerCenter: CustomerCenter let lastPublishedAppVersion: String? - let productId: UInt + let itunesTrackId: UInt? struct CustomerCenter { From d27f8a47dc986ffe92cc7250746e56fa097d66de Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:40:16 +0200 Subject: [PATCH 81/90] Updates a comment. --- RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index d13c1e83bf..4d3e226922 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -34,7 +34,7 @@ struct AppUpdateWarningView: View { init(productId: UInt, onContinueAnywayClick: @escaping () -> Void) { self.init( onUpdateAppClick: { - // productId is a positive integer, so it is safe to construct a URL from it. + // productId is a positive integer, so it should be safe to construct a URL from it. UIApplication.shared.open(URL(string: "https://itunes.apple.com/app/id\(productId)")!) }, onContinueAnywayClick: onContinueAnywayClick From d68c280d881d55a287029680b571fd90e92b9ead Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:41:39 +0200 Subject: [PATCH 82/90] Fixes a constructor. --- Sources/CustomerCenter/CustomerCenterConfigData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 68a146fc9f..f3be6dbf70 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -34,7 +34,7 @@ public struct CustomerCenterConfigData { localization: Localization, support: Support, lastPublishedAppVersion: String?, - productId: UInt) { + productId: UInt?) { self.screens = screens self.appearance = appearance self.localization = localization From 84cc70b7f922c2306f54967a4704e909037a9fc3 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:13:57 +0200 Subject: [PATCH 83/90] Restyles AppUpdateWarningView in line with the updated WrongPlatformView. --- .../Views/AppUpdateWarningView.swift | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 30c2e549a7..60b4bae296 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -39,30 +39,29 @@ struct AppUpdateWarningView: View { @ViewBuilder var content: some View { ZStack { - if let background = Color.from(colorInformation: appearance.backgroundColor, for: colorScheme) { - background.edgesIgnoringSafeArea(.all) - } - let textColor = Color.from(colorInformation: appearance.textColor, for: colorScheme) - - VStack { - CompatibilityContentUnavailableView( - localization.commonLocalizedString(for: .updateWarningTitle), - systemImage: "arrowshape.up.circle.fill", - description: Text(localization.commonLocalizedString(for: .updateWarningDescription)) - ) - - Button(localization.commonLocalizedString(for: .updateWarningUpdate)) { - onUpdateAppClick() + List { + Section { + CompatibilityContentUnavailableView( + localization.commonLocalizedString(for: .updateWarningTitle), + systemImage: "arrowshape.up.circle.fill", + description: Text(localization.commonLocalizedString(for: .updateWarningDescription)) + ) } - .buttonStyle(ProminentButtonStyle()) - .padding(.bottom) - Button(localization.commonLocalizedString(for: .updateWarningIgnore)) { - onContinueAnywayClick() + Section { + Button(localization.commonLocalizedString(for: .updateWarningUpdate)) { + onUpdateAppClick() + } + .buttonStyle(ProminentButtonStyle()) + .padding(.top, 4) + + Button(localization.commonLocalizedString(for: .updateWarningIgnore)) { + onContinueAnywayClick() + } + .buttonStyle(TextButtonStyle()) } + .listRowSeparator(.hidden) } - .padding(.horizontal) - .applyIf(textColor != nil, apply: { $0.foregroundColor(textColor) }) } .toolbar { ToolbarItem(placement: .compatibleTopBarTrailing) { @@ -86,6 +85,22 @@ struct AppUpdateWarningView: View { } } +/// This is a workaround to be able to have 2 buttons in a single Section. Buttons without ButtonStyles make the entire +/// section clickable. +@available(iOS 15.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private struct TextButtonStyle: PrimitiveButtonStyle { + + func makeBody(configuration: PrimitiveButtonStyleConfiguration) -> some View { + Button(action: { configuration.trigger() }, label: { + configuration.label.frame(maxWidth: .infinity) + }) + } + +} + #if DEBUG @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) From 191a41b7112de56bdedd6f448f172159b3ae7a34 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:04:44 +0200 Subject: [PATCH 84/90] Fixes a test --- .../CustomerCenter/CustomerCenterConfigDataTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index eea10e6bbf..21d491fb63 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -83,7 +83,8 @@ class CustomerCenterConfigDataTests: TestCase { localization: .init(locale: "en_US", localizedStrings: ["key": "value"]), support: .init(email: "support@example.com") ), - lastPublishedAppVersion: "1.2.3" + lastPublishedAppVersion: "1.2.3", + itunesTrackId: 123 ) let configData = CustomerCenterConfigData(from: mockResponse) @@ -139,6 +140,7 @@ class CustomerCenterConfigDataTests: TestCase { } expect(configData.lastPublishedAppVersion) == "1.2.3" + expect(configData.itunesTrackId) == 123 } } From 0d65d2ba63799d090a2a43c39a4193c9e33b4373 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:11:26 +0200 Subject: [PATCH 85/90] Fixes a test again --- .../CustomerCenter/CustomerCenterConfigDataTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift index 21d491fb63..6550ea68ba 100644 --- a/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift +++ b/Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift @@ -140,7 +140,7 @@ class CustomerCenterConfigDataTests: TestCase { } expect(configData.lastPublishedAppVersion) == "1.2.3" - expect(configData.itunesTrackId) == 123 + expect(configData.productId) == 123 } } From 642253352b9e4cc05f190a42312ee4afa212fc61 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:11:50 +0200 Subject: [PATCH 86/90] Updates the dismiss button. --- .../CustomerCenter/Views/AppUpdateWarningView.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index 60b4bae296..e9bf766504 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -26,9 +26,6 @@ struct AppUpdateWarningView: View { let onUpdateAppClick: () -> Void let onContinueAnywayClick: () -> Void - @Environment(\.dismiss) - var dismiss - @Environment(\.localization) private var localization: CustomerCenterConfigData.Localization @Environment(\.appearance) @@ -65,9 +62,7 @@ struct AppUpdateWarningView: View { } .toolbar { ToolbarItem(placement: .compatibleTopBarTrailing) { - DismissCircleButton { - dismiss() - } + DismissCircleButton() } } } From acb4f688f53b87962d69f74579fd284da613e41f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:09:53 +0200 Subject: [PATCH 87/90] Updates the copy. --- Sources/CustomerCenter/CustomerCenterConfigData.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 016361cfe8..cb5f44e4cb 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -123,12 +123,9 @@ public struct CustomerCenterConfigData { case .dismiss: return "Dismiss" case .updateWarningTitle: - return "Update the app to fix the problem." + return "Update available" case .updateWarningDescription: - return """ - A new update is available. Downloading the latest version of the app may help solve the \ - problem. - """ + return "Downloading the latest version of the app may help solve the problem." case .updateWarningUpdate: return "Update" case .updateWarningIgnore: From 5e3bebeae7cf6dc0a77752dd7b8bc8d3f25f4361 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:10:12 +0200 Subject: [PATCH 88/90] Updates the icon. --- RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift index e9bf766504..c8444b83f8 100644 --- a/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/RevenueCatUI/CustomerCenter/Views/AppUpdateWarningView.swift @@ -40,7 +40,7 @@ struct AppUpdateWarningView: View { Section { CompatibilityContentUnavailableView( localization.commonLocalizedString(for: .updateWarningTitle), - systemImage: "arrowshape.up.circle.fill", + systemImage: "arrow.up.circle.fill", description: Text(localization.commonLocalizedString(for: .updateWarningDescription)) ) } From a55224efbe047f7c197fd58a64da74cf8adef4d2 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:41:35 +0200 Subject: [PATCH 89/90] Safely unwraps configuration.productId. --- .../CustomerCenter/Views/CustomerCenterView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index dbfcaa569a..305ac0d6a2 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -95,18 +95,18 @@ private extension CustomerCenterView { if viewModel.hasSubscriptions { if viewModel.subscriptionsAreFromApple, let screen = configuration.screens[.management] { - if ignoreAppUpdateWarning || viewModel.appIsLatestVersion || configuration.productId == nil { - ManageSubscriptionsView(screen: screen, - customerCenterActionHandler: viewModel.customerCenterActionHandler) - } else { + if let productId = configuration.productId, !ignoreAppUpdateWarning && !viewModel.appIsLatestVersion { AppUpdateWarningView( - productId: configuration.productId!, + productId: productId, onContinueAnywayClick: { withAnimation { ignoreAppUpdateWarning = true } } ) + } else { + ManageSubscriptionsView(screen: screen, + customerCenterActionHandler: viewModel.customerCenterActionHandler) } } else { WrongPlatformView() From 7398e535fa6396051d5d75a8f11e5833bfc4ea87 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:42:10 +0200 Subject: [PATCH 90/90] Removes an accidental redecleration after a merge. --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 305ac0d6a2..5de06febf0 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -29,7 +29,6 @@ public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel @State private var ignoreAppUpdateWarning: Bool = false - @State private var ignoreAppUpdateWarning: Bool = false @Environment(\.colorScheme) private var colorScheme