diff --git a/.circleci/config.yml b/.circleci/config.yml index e08f619ffd..321431b893 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,7 +70,7 @@ aliases: release-tags: &release-tags filters: tags: - ignore: + ignore: - /^.*-SNAPSHOT/ - /^.*-customercenter.alpha.*/ branches: @@ -1282,6 +1282,19 @@ jobs: name: Submit Purchase Tester command: bundle exec fastlane deploy_purchase_tester dry_run:<< parameters.dry_run >> + emerge_purchases_ui_snapshot_tests: + executor: + name: macos-executor + steps: + - checkout + - setup-git-credentials + - trust-github-key + - install-dependencies + - update-spm-installation-commit + - run: + name: Build Paywalls Tester + command: bundle exec fastlane build_paywalls_tester_for_emerge + deploy-to-spm: docker: - image: cimg/base:stable @@ -1335,13 +1348,14 @@ workflows: - pod-lib-lint - run-revenuecat-ui-ios-17 - run-revenuecat-ui-ios-18 + - emerge_purchases_ui_snapshot_tests create-tag: when: and: - not: equal: [scheduled_pipeline, << pipeline.trigger_source >>] - - matches: + - matches: pattern: "^release/.*$" value: << pipeline.git.branch >> jobs: @@ -1426,7 +1440,7 @@ workflows: - docs-deploy # To trigger tests manually, log into circleCI, select the project, a branch, and then click "Trigger Pipeline" - # in the top right hand corner of the screen. In the modal that appears, set the following parameter fields + # in the top right hand corner of the screen. In the modal that appears, set the following parameter fields # and click "Trigger Pipeline" to begin the pipeline: # - Parameter Type: string # - Name: action @@ -1434,13 +1448,13 @@ workflows: all-tests: when: or: - - matches: + - matches: pattern: "^release/.*$" value: << pipeline.git.branch >> - equal: - "run-manual-tests" - << pipeline.parameters.action >> - - equal: + - equal: - "run-from-github-comments" - << pipeline.parameters.GHA_Meta >> jobs: @@ -1475,4 +1489,5 @@ workflows: - api-tests - deploy-purchase-tester: dry_run: true - + - emerge_purchases_ui_snapshot_tests + diff --git a/Gemfile.lock b/Gemfile.lock index b6f4aac34d..f06690193c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -206,6 +206,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-emerge (0.10.6) + faraday (~> 1.1) fastlane-sirp (1.0.0) sysrandom (~> 1.0) ffi (1.17.0-arm64-darwin) @@ -371,6 +373,7 @@ DEPENDENCIES danger fastlane fastlane-plugin-create_xcframework! + fastlane-plugin-emerge fastlane-plugin-revenuecat_internal! nokogiri rest-client diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 86f92f4bfc..51e69becfe 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ 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 */; }; + 3531DF882CFE138D00D454BF /* ManageSubscriptionsButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3531DF872CFE138800D454BF /* ManageSubscriptionsButtonsView.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 */; }; @@ -313,6 +314,7 @@ 35D83300262FAD8000E60AC5 /* ETagManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D832FF262FAD8000E60AC5 /* ETagManagerTests.swift */; }; 35D8330A262FBA9A00E60AC5 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E357D16038F07915D7825D /* MockUserDefaults.swift */; }; 35D83312262FBD4200E60AC5 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; }; + 35DE0DB62CEF9E8F00EB83E9 /* SubscriptionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.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 */; }; @@ -1182,6 +1184,13 @@ remoteGlobalIDString = 2DEAC2D926EFE46E006914ED; remoteInfo = UnitTestsHostApp; }; + 4D6F4BE82CFE2FAB00353AF6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2DD5008F2C519EB4009C19B7 /* PaywallsTester.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FA29FBA82CCAA79800DA1976; + remoteInfo = PaywallsTesterTests; + }; 4F6BEE082A27B02400CD9322 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; @@ -1511,6 +1520,7 @@ 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 = ""; }; + 3531DF872CFE138800D454BF /* ManageSubscriptionsButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsButtonsView.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 = ""; }; @@ -1576,6 +1586,7 @@ 35D832F3262E606500E60AC5 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; 35D832FF262FAD8000E60AC5 /* ETagManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ETagManagerTests.swift; sourceTree = ""; }; 35D83311262FBD4200E60AC5 /* MockETagManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockETagManager.swift; sourceTree = ""; }; + 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfo.swift; sourceTree = ""; }; 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 = ""; }; @@ -1643,6 +1654,7 @@ 4D6ABB0D2AF13FB100BB2A08 /* StoreEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreEnvironment.swift; sourceTree = ""; }; 4D6ABB0F2AF13FBD00BB2A08 /* SK2AppTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SK2AppTransaction.swift; sourceTree = ""; }; 4D6F4BCF2CF69DE300353AF6 /* ForegroundColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundColorScheme.swift; sourceTree = ""; }; + 4D6F4BE52CFE2FAB00353AF6 /* PaywallsTesterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallsTesterTests.swift; sourceTree = ""; }; 4D7A3E272B85729E00ABDE67 /* PurchasesOrchestratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesOrchestratorTests.swift; sourceTree = ""; }; 4DBC30952B1DFA97001D33C7 /* StoreKitVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitVersion.swift; sourceTree = ""; }; 4DBF1F352B4D572400D52354 /* LocalReceiptFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalReceiptFetcher.swift; sourceTree = ""; }; @@ -3011,6 +3023,7 @@ children = ( 2DD500402C519EB4009C19B7 /* ci_scripts */, 2DD5008E2C519EB4009C19B7 /* PaywallsTester */, + 4D6F4BE62CFE2FAB00353AF6 /* PaywallsTesterTests */, 2DD5008F2C519EB4009C19B7 /* PaywallsTester.xcodeproj */, 2DD500902C519EB4009C19B7 /* PaywallsTester.xcworkspace */, 2DD500912C519EB4009C19B7 /* Postprocessor.sh */, @@ -3166,6 +3179,7 @@ isa = PBXGroup; children = ( 2DD500F82C519EB4009C19B7 /* PaywallsTester.app */, + 4D6F4BE92CFE2FAB00353AF6 /* PaywallsTesterTests.xctest */, ); name = Products; sourceTree = ""; @@ -3528,6 +3542,7 @@ 353756602C382C2800A1B8D6 /* Views */ = { isa = PBXGroup; children = ( + 3531DF872CFE138800D454BF /* ManageSubscriptionsButtonsView.swift */, 2D2AFE8E2C6A9D8700D1B0B4 /* CompatibilityContentUnavailableView.swift */, 2C4C36122C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift */, 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */, @@ -3894,6 +3909,14 @@ path = BasicTypes; sourceTree = ""; }; + 4D6F4BE62CFE2FAB00353AF6 /* PaywallsTesterTests */ = { + isa = PBXGroup; + children = ( + 4D6F4BE52CFE2FAB00353AF6 /* PaywallsTesterTests.swift */, + ); + path = PaywallsTesterTests; + sourceTree = ""; + }; 4F1428A52A4A1330006CD196 /* Test Data */ = { isa = PBXGroup; children = ( @@ -4872,6 +4895,7 @@ B3A36AAC26BC76230059EDEA /* Identity */ = { isa = PBXGroup; children = ( + 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.swift */, A56F9AB026990E9200AFC48F /* CustomerInfo.swift */, 57F3C10429B7B22E0004FD7E /* CustomerInfo+ActiveDates.swift */, 4F15B4A02A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift */, @@ -5436,6 +5460,13 @@ remoteRef = 2DD501092C519EB4009C19B7 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 4D6F4BE92CFE2FAB00353AF6 /* PaywallsTesterTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = PaywallsTesterTests.xctest; + remoteRef = 4D6F4BE82CFE2FAB00353AF6 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -5847,6 +5878,7 @@ FD43D2FC2C41864000077235 /* TimeInterval+Extensions.swift in Sources */, 4F7DBFBD2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift in Sources */, 5766AB4728401B8400FA6091 /* PackageType.swift in Sources */, + 35DE0DB62CEF9E8F00EB83E9 /* SubscriptionInfo.swift in Sources */, B3F3E8DA277158FE0047A5B9 /* DNSChecker.swift in Sources */, 1EB697862CD0ED0B003000FC /* WebPurchaseRedemptionResult.swift in Sources */, A525BF4B26C320D100C354C4 /* SubscriberAttributesManager.swift in Sources */, @@ -6564,6 +6596,7 @@ 77BA1AB32CCBB6EE009BF0EA /* RootView.swift in Sources */, 887A606C2C1D037000E1A461 /* Constants.swift in Sources */, 887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */, + 3531DF882CFE138D00D454BF /* ManageSubscriptionsButtonsView.swift in Sources */, 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */, 2CC791562CC0452100FBE120 /* PackageComponentView.swift in Sources */, 2CC791592CC0452100FBE120 /* PurchaseButtonComponentView.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 8d95b4cea6..fa724ab8ea 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -27,6 +27,7 @@ import SwiftUI class ManageSubscriptionsViewModel: ObservableObject { let screen: CustomerCenterConfigData.Screen + let paths: [CustomerCenterConfigData.HelpPath] @Published var showRestoreAlert: Bool = false @@ -66,6 +67,7 @@ class ManageSubscriptionsViewModel: ObservableObject { purchasesProvider: ManageSubscriptionsPurchaseType = ManageSubscriptionPurchases(), loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType? = nil) { self.screen = screen + self.paths = screen.filteredPaths self.purchasesProvider = purchasesProvider self.customerCenterActionHandler = customerCenterActionHandler self.loadPromotionalOfferUseCase = loadPromotionalOfferUseCase ?? LoadPromotionalOfferUseCase() @@ -77,6 +79,7 @@ class ManageSubscriptionsViewModel: ObservableObject { customerCenterActionHandler: CustomerCenterActionHandler?, refundRequestStatus: RefundRequestStatus? = nil) { self.screen = screen + self.paths = screen.filteredPaths self.purchaseInformation = purchaseInformation self.purchasesProvider = ManageSubscriptionPurchases() self.refundRequestStatus = refundRequestStatus @@ -262,4 +265,18 @@ private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType } +private extension CustomerCenterConfigData.Screen { + + var filteredPaths: [CustomerCenterConfigData.HelpPath] { + return self.paths.filter { path in + #if targetEnvironment(macCatalyst) + return path.type == .refundRequest + #else + return path.type != .unknown + #endif + } + } + +} + #endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index d7c4af36aa..a9a030c6f7 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -113,7 +113,13 @@ private extension CustomerCenterView { WrongPlatformView() } } else { - NoSubscriptionsView(configuration: configuration) + if let screen = configuration.screens[.noActive] { + ManageSubscriptionsView(screen: screen, + customerCenterActionHandler: viewModel.customerCenterActionHandler) + } else { + // Fallback with a restore button + NoSubscriptionsView(configuration: configuration) + } } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsButtonsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsButtonsView.swift new file mode 100644 index 0000000000..5e28869df2 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsButtonsView.swift @@ -0,0 +1,70 @@ +// +// 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 +// +// ManageSubscriptionsButtonsView.swift +// +// Created by Cesar de la Vega on 2/12/24. + +import Foundation +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct ManageSubscriptionsButtonsView: View { + + @ObservedObject + var viewModel: ManageSubscriptionsViewModel + @Binding + var loadingPath: CustomerCenterConfigData.HelpPath? + @Environment(\.openURL) + var openURL + + @Environment(\.localization) + private var localization: CustomerCenterConfigData.Localization + + var body: some View { + ForEach(self.viewModel.paths, id: \.id) { path in + ManageSubscriptionButton(path: path, viewModel: self.viewModel) + } + } + +} + +@available(iOS 15.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +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 { + TintedProgressView() + } else { + Text(path.title) + } + }) + .disabled(self.viewModel.loadingPath != nil) + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 31c9f9e7af..bf8ffe6ebe 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -85,17 +85,32 @@ struct ManageSubscriptionsView: View { SubscriptionDetailsView(purchaseInformation: purchaseInformation, refundRequestStatus: self.viewModel.refundRequestStatus) } - } + Section { + ManageSubscriptionsButtonsView(viewModel: self.viewModel, + loadingPath: self.$viewModel.loadingPath) + } header: { + if let subtitle = self.viewModel.screen.subtitle { + Text(subtitle) + .textCase(nil) + } + } + } else { + let fallbackDescription = localization.commonLocalizedString(for: .tryCheckRestore) - Section { - ManageSubscriptionsButtonsView(viewModel: self.viewModel, - loadingPath: self.$viewModel.loadingPath) - } header: { - if let subtitle = self.viewModel.screen.subtitle { - Text(subtitle) - .textCase(nil) + Section { + CompatibilityContentUnavailableView( + self.viewModel.screen.title, + systemImage: "exclamationmark.triangle.fill", + description: Text(self.viewModel.screen.subtitle ?? fallbackDescription) + ) + } + + Section { + ManageSubscriptionsButtonsView(viewModel: self.viewModel, + loadingPath: self.$viewModel.loadingPath) } } + } } else { TintedProgressView() @@ -131,8 +146,10 @@ struct ManageSubscriptionsView: View { }, content: { inAppBrowserURL in SafariView(url: inAppBrowserURL.url) }) - .navigationTitle(self.viewModel.screen.title) - .navigationBarTitleDisplayMode(.inline) + .applyIf(self.viewModel.screen.type == .management, apply: { + $0.navigationTitle(self.viewModel.screen.title).navigationBarTitleDisplayMode(.inline) + }) + } } @@ -151,63 +168,6 @@ private extension ManageSubscriptionsView { } -@available(iOS 15.0, *) -@available(macOS, unavailable) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -struct ManageSubscriptionsButtonsView: View { - - @ObservedObject - var viewModel: ManageSubscriptionsViewModel - @Binding - var loadingPath: CustomerCenterConfigData.HelpPath? - @Environment(\.openURL) - var openURL - - @Environment(\.localization) - private var localization: CustomerCenterConfigData.Localization - - var body: some View { - let filteredPaths = self.viewModel.screen.paths.filter { path in -#if targetEnvironment(macCatalyst) - return path.type == .refundRequest -#else - return path.type != .unknown -#endif - } - ForEach(filteredPaths, id: \.id) { path in - ManageSubscriptionButton(path: path, viewModel: self.viewModel) - } - } - -} - -@available(iOS 15.0, *) -@available(macOS, unavailable) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -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 { - TintedProgressView() - } else { - Text(path.title) - } - }) - .disabled(self.viewModel.loadingPath != nil) - } - -} - #if DEBUG @available(iOS 15.0, *) @available(macOS, unavailable) diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index b584369114..b6fddbc91c 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -40,12 +40,13 @@ struct NoSubscriptionsView: View { } var body: some View { - let fallbackDescription = "We can try checking your Apple account for any previous purchases" + let fallbackDescription = localization.commonLocalizedString(for: .tryCheckRestore) + let fallbackTitle = localization.commonLocalizedString(for: .noSubscriptionsFound) List { Section { CompatibilityContentUnavailableView( - self.configuration.screens[.noActive]?.title ?? "No subscriptions found", + self.configuration.screens[.noActive]?.title ?? fallbackTitle, systemImage: "exclamationmark.triangle.fill", description: Text(self.configuration.screens[.noActive]?.subtitle ?? fallbackDescription) @@ -57,10 +58,6 @@ struct NoSubscriptionsView: View { showRestoreAlert = true } .restorePurchasesAlert(isPresented: $showRestoreAlert) - } header: { - let subtitle = localization.commonLocalizedString(for: .tryCheckRestore) - Text(subtitle) - .textCase(nil) } } diff --git a/RevenueCatUI/Resources/fi.lproj/Localizable.strings b/RevenueCatUI/Resources/fi.lproj/Localizable.strings index 158a300b57..d455a3544d 100644 --- a/RevenueCatUI/Resources/fi.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/fi.lproj/Localizable.strings @@ -2,9 +2,9 @@ "All subscriptions" = "Kaikki tilaukset"; "Privacy" = "Yksityisyys"; "Privacy policy" = "Tietosuojakäytäntö"; -"Purchases restored successfully!" = "Ostokset palautettu onnistuneesti!"; -"Restore" = "Palauttaa"; -"Restore purchases" = "Palauttaa ostot"; +"Purchases restored successfully!" = "Ostot palautettu onnistuneesti!"; +"Restore" = "Palauta"; +"Restore purchases" = "Palauta ostot"; "Terms" = "Ehdot"; "Terms and conditions" = "Käyttöehdot"; "Annual" = "Vuosittainen"; diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index a7bd7cae06..03a4587190 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -12,8 +12,14 @@ // Created by Madeline Beyl on 7/9/21. // +// swiftlint:disable file_length import Foundation +/** + An identifier used to identify a product. + */ +public typealias ProductIdentifier = String + /** A container for the most recent customer info returned from `Purchases`. These objects are non-mutable and do not update automatically. @@ -24,10 +30,12 @@ import Foundation @objc public let entitlements: EntitlementInfos /// All *subscription* product identifiers with expiration dates in the future. - @objc public var activeSubscriptions: Set { self.activeKeys(dates: self.expirationDatesByProductId) } + @objc public var activeSubscriptions: Set { + self.activeKeys(dates: self.expirationDatesByProductId) + } /// All product identifiers purchases by the user regardless of expiration. - @objc public let allPurchasedProductIdentifiers: Set + @objc public let allPurchasedProductIdentifiers: Set /// Returns the latest expiration date of all products, nil if there are none. @objc public var latestExpirationDate: Date? { @@ -88,17 +96,20 @@ import Foundation */ @objc public let originalApplicationVersion: String? + /// Dictionary of all subscription product identifiers and their subscription info + @objc public let subscriptionsByProductIdentifier: [ProductIdentifier: SubscriptionInfo] + /// Get the expiration date for a given product identifier. You should use Entitlements though! /// - Parameter productIdentifier: Product identifier for product /// - Returns: The expiration date for `productIdentifier`, `nil` if product never purchased - @objc public func expirationDate(forProductIdentifier productIdentifier: String) -> Date? { + @objc public func expirationDate(forProductIdentifier productIdentifier: ProductIdentifier) -> Date? { return expirationDatesByProductId[productIdentifier] ?? nil } /// Get the latest purchase or renewal date for a given product identifier. You should use Entitlements though! /// - Parameter productIdentifier: Product identifier for subscription product /// - Returns: The purchase date for `productIdentifier`, `nil` if product never purchased - @objc public func purchaseDate(forProductIdentifier productIdentifier: String) -> Date? { + @objc public func purchaseDate(forProductIdentifier productIdentifier: ProductIdentifier) -> Date? { return purchaseDatesByProductId[productIdentifier] ?? nil } @@ -143,6 +154,8 @@ import Foundation let verificationResult = self.entitlements.verification.debugDescription + let subscriptionsDescription = self.subscriptionsByProductIdentifier.mapValues { $0.description } + return """ <\(String(describing: CustomerInfo.self)): originalApplicationVersion=\(self.originalApplicationVersion ?? ""), @@ -150,6 +163,7 @@ import Foundation activeEntitlements=\(activeEntitlementsDescription), activeSubscriptions=\(activeSubsDescription), nonSubscriptions=\(self.nonSubscriptions), + subscriptions=\(subscriptionsDescription), requestDate=\(String(describing: self.requestDate)), firstSeen=\(String(describing: self.firstSeen)), originalAppUserId=\(self.originalAppUserId), @@ -208,6 +222,26 @@ import Foundation self.purchaseDatesByProductId = Self.extractPurchaseDates(subscriber) self.allPurchasedProductIdentifiers = Set(self.expirationDatesByProductId.keys) .union(self.nonSubscriptions.map { $0.productIdentifier }) + + self.subscriptionsByProductIdentifier = + Dictionary(uniqueKeysWithValues: subscriber.subscriptions.map { (key, subscriptionData) in + (key, SubscriptionInfo( + productIdentifier: key, + purchaseDate: subscriptionData.purchaseDate, + originalPurchaseDate: subscriptionData.originalPurchaseDate, + expiresDate: subscriptionData.expiresDate, + store: subscriptionData.store, + isSandbox: subscriptionData.isSandbox, + unsubscribeDetectedAt: subscriptionData.unsubscribeDetectedAt, + billingIssuesDetectedAt: subscriptionData.billingIssuesDetectedAt, + gracePeriodExpiresDate: subscriptionData.gracePeriodExpiresDate, + ownershipType: subscriptionData.ownershipType, + periodType: subscriptionData.periodType, + refundedAt: subscriptionData.refundedAt, + storeTransactionId: subscriptionData.storeTransactionId, + requestDate: response.requestDate + )) + }) } private let expirationDatesByProductId: [String: Date?] diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift new file mode 100644 index 0000000000..c79e9a7f32 --- /dev/null +++ b/Sources/Identity/SubscriptionInfo.swift @@ -0,0 +1,134 @@ +// +// 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 +// +// SubscriptionInfo.swift +// +// Created by Cesar de la Vega on 21/11/24. + +import Foundation + +/// Subscription purchases of the Customer +@objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { + + /// The product identifier. + @objc public let productIdentifier: ProductIdentifier + + /// Date when the last subscription period started. + @objc public let purchaseDate: Date + + /// Date when this subscription first started. This property does not update with renewals. + /// This property also does not update for product changes within a subscription group or + /// resubscriptions by lapsed subscribers. + @objc public let originalPurchaseDate: Date? + + /// Date when the subscription expires/expired + @objc public let expiresDate: Date? + + /// Store where the subscription was purchased. + @objc public let store: Store + + /// Whether or not the purchase was made in sandbox mode. + @objc public let isSandbox: Bool + + /// Date when RevenueCat detected that auto-renewal was turned off for this subsription. + /// Note the subscription may still be active, check the ``expiresDate`` attribute. + @objc public let unsubscribeDetectedAt: Date? + + /// Date when RevenueCat detected any billing issues with this subscription. + /// If and when the billing issue gets resolved, this field is set to nil. + /// Note the subscription may still be active, check the ``expiresDate`` attribute. + @objc public let billingIssuesDetectedAt: Date? + + /// Date when any grace period for this subscription expires/expired. + /// nil if the customer has never been in a grace period. + @objc public let gracePeriodExpiresDate: Date? + + /// How the Customer received access to this subscription: + /// - ``PurchaseOwnershipType/purchased``: The customer bought the subscription. + /// - ``PurchaseOwnershipType/familyShared``: The Customer has access to the product via their family. + @objc public let ownershipType: PurchaseOwnershipType + + /// Type of the current subscription period: + /// - ``PeriodType/normal``: The product is in a normal period (default) + /// - ``PeriodType/trial``: The product is in a free trial period + /// - ``PeriodType/intro``: The product is in an introductory pricing period + @objc public let periodType: PeriodType + + /// Date when RevenueCat detected a refund of this subscription. + @objc public let refundedAt: Date? + + /// The transaction id in the store of the subscription. + @objc public let storeTransactionId: String? + + /// Whether the subscription is currently active. + @objc public let isActive: Bool + + /// Whether the subscription will renew at the next billing period. + @objc public let willRenew: Bool + + init(productIdentifier: String, + purchaseDate: Date, + originalPurchaseDate: Date?, + expiresDate: Date?, + store: Store, + isSandbox: Bool, + unsubscribeDetectedAt: Date?, + billingIssuesDetectedAt: Date?, + gracePeriodExpiresDate: Date?, + ownershipType: PurchaseOwnershipType, + periodType: PeriodType, + refundedAt: Date?, + storeTransactionId: String?, + requestDate: Date) { + self.productIdentifier = productIdentifier + self.purchaseDate = purchaseDate + self.originalPurchaseDate = originalPurchaseDate + self.expiresDate = expiresDate + self.store = store + self.isSandbox = isSandbox + self.unsubscribeDetectedAt = unsubscribeDetectedAt + self.billingIssuesDetectedAt = billingIssuesDetectedAt + self.gracePeriodExpiresDate = gracePeriodExpiresDate + self.ownershipType = ownershipType + self.periodType = periodType + self.refundedAt = refundedAt + self.storeTransactionId = storeTransactionId + self.isActive = CustomerInfo.isDateActive(expirationDate: expiresDate, for: requestDate) + self.willRenew = EntitlementInfo.willRenewWithExpirationDate(expirationDate: expiresDate, + store: store, + unsubscribeDetectedAt: unsubscribeDetectedAt, + billingIssueDetectedAt: billingIssuesDetectedAt) + + super.init() + } + + public override var description: String { + return """ + SubscriptionInfo { + purchaseDate: \(String(describing: purchaseDate)), + originalPurchaseDate: \(String(describing: originalPurchaseDate)), + expiresDate: \(String(describing: expiresDate)), + store: \(store), + isSandbox: \(isSandbox), + unsubscribeDetectedAt: \(String(describing: unsubscribeDetectedAt)), + billingIssuesDetectedAt: \(String(describing: billingIssuesDetectedAt)), + gracePeriodExpiresDate: \(String(describing: gracePeriodExpiresDate)), + ownershipType: \(ownershipType), + periodType: \(String(describing: periodType)), + refundedAt: \(String(describing: refundedAt)), + storeTransactionId: \(String(describing: storeTransactionId)), + isActive: \(isActive), + willRenew: \(willRenew) + } + """ + } + +} + +extension SubscriptionInfo: Sendable {} diff --git a/Sources/Networking/Responses/CustomerInfoResponse.swift b/Sources/Networking/Responses/CustomerInfoResponse.swift index 3d3513fe4b..230db607bc 100644 --- a/Sources/Networking/Responses/CustomerInfoResponse.swift +++ b/Sources/Networking/Responses/CustomerInfoResponse.swift @@ -49,7 +49,7 @@ extension CustomerInfoResponse { @IgnoreDecodeErrors var periodType: PeriodType - var purchaseDate: Date? + var purchaseDate: Date var originalPurchaseDate: Date? var expiresDate: Date? @IgnoreDecodeErrors @@ -62,12 +62,15 @@ extension CustomerInfoResponse { var ownershipType: PurchaseOwnershipType var productPlanIdentifier: String? var metadata: [String: String]? + var gracePeriodExpiresDate: Date? + var refundedAt: Date? + var storeTransactionId: String? } struct Transaction { - var purchaseDate: Date? + var purchaseDate: Date var originalPurchaseDate: Date? var transactionIdentifier: String? var storeTransactionIdentifier: String? @@ -174,7 +177,7 @@ extension CustomerInfoResponse.Subscriber { extension CustomerInfoResponse.Transaction { init( - purchaseDate: Date?, + purchaseDate: Date, originalPurchaseDate: Date?, transactionIdentifier: String?, storeTransactionIdentifier: String?, @@ -202,14 +205,15 @@ extension CustomerInfoResponse.Subscription { init( periodType: PeriodType = .defaultValue, - purchaseDate: Date? = nil, + purchaseDate: Date, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, store: Store = .defaultValue, isSandbox: Bool, unsubscribeDetectedAt: Date? = nil, billingIssuesDetectedAt: Date? = nil, - ownershipType: PurchaseOwnershipType = .defaultValue + ownershipType: PurchaseOwnershipType = .defaultValue, + storeTransactionId: String? = nil ) { self.periodType = periodType self.purchaseDate = purchaseDate @@ -220,6 +224,7 @@ extension CustomerInfoResponse.Subscription { self.unsubscribeDetectedAt = unsubscribeDetectedAt self.billingIssuesDetectedAt = billingIssuesDetectedAt self.ownershipType = ownershipType + self.storeTransactionId = storeTransactionId } var asTransaction: CustomerInfoResponse.Transaction { diff --git a/Sources/Purchasing/EntitlementInfo.swift b/Sources/Purchasing/EntitlementInfo.swift index 987584e773..2f24ff4e5b 100644 --- a/Sources/Purchasing/EntitlementInfo.swift +++ b/Sources/Purchasing/EntitlementInfo.swift @@ -299,7 +299,7 @@ public extension EntitlementInfo { // MARK: - Internal -private extension EntitlementInfo { +extension EntitlementInfo { static func willRenewWithExpirationDate(expirationDate: Date?, store: Store, diff --git a/Sources/Purchasing/NonSubscriptionTransaction.swift b/Sources/Purchasing/NonSubscriptionTransaction.swift index a6b66406be..9f66970ec5 100644 --- a/Sources/Purchasing/NonSubscriptionTransaction.swift +++ b/Sources/Purchasing/NonSubscriptionTransaction.swift @@ -34,10 +34,14 @@ public final class NonSubscriptionTransaction: NSObject { /// The unique identifier for the transaction created by the Store. @objc public let storeTransactionIdentifier: String + /** + * The ``Store`` where this transaction was performed. + */ + @objc public let store: Store + init?(with transaction: CustomerInfoResponse.Transaction, productID: String) { guard let transactionIdentifier = transaction.transactionIdentifier, - let storeTransactionIdentifier = transaction.storeTransactionIdentifier, - let purchaseDate = transaction.purchaseDate else { + let storeTransactionIdentifier = transaction.storeTransactionIdentifier else { Logger.error("Couldn't initialize NonSubscriptionTransaction. " + "Reason: missing data: \(transaction).") return nil @@ -45,8 +49,9 @@ public final class NonSubscriptionTransaction: NSObject { self.transactionIdentifier = transactionIdentifier self.storeTransactionIdentifier = storeTransactionIdentifier - self.purchaseDate = purchaseDate + self.purchaseDate = transaction.purchaseDate self.productIdentifier = productID + self.store = transaction.store } public override var description: String { diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 723035beb9..200d352a03 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -123,6 +123,9 @@ 2D4C62D62C5D41E200A29FD2 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D42C5D41E200A29FD2 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2D4C62D92C5D41EC00A29FD2 /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; }; 2D4C62DA2C5D41EC00A29FD2 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */; }; + 35370AC52CFF8304004F0A64 /* RCSubscriptionInfoAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */; }; + 35370AC82CFF8317004F0A64 /* RCSubscriptionInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -346,6 +349,9 @@ 2D4C62D02C5D41D400A29FD2 /* ReceiptParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReceiptParser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C62D42C5D41E200A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoAPI.swift; sourceTree = ""; }; + 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCSubscriptionInfoAPI.h; sourceTree = ""; }; + 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCSubscriptionInfoAPI.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -460,6 +466,8 @@ 2D4C613E2C5AD30400A29FD2 /* ObjcAPITester */ = { isa = PBXGroup; children = ( + 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */, + 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */, 2D4C61732C5AD31900A29FD2 /* main.m */, 2D4C61582C5AD31600A29FD2 /* RCAttributionAPI.h */, 2D4C614C2C5AD31500A29FD2 /* RCAttributionAPI.m */, @@ -533,6 +541,7 @@ 2D4C61B42C5AD61800A29FD2 /* SwiftAPITester */ = { isa = PBXGroup; children = ( + 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */, 2D4C61D32C5AD62900A29FD2 /* AttributionNetworkAPI.swift */, 2D4C61CC2C5AD62900A29FD2 /* AttributionAPI.swift */, 2D4C61D42C5AD62900A29FD2 /* ConfigurationAPI.swift */, @@ -643,6 +652,7 @@ buildActionMask = 2147483647; files = ( 2D4C61402C5AD30400A29FD2 /* ObjcAPITester.h in Headers */, + 35370AC52CFF8304004F0A64 /* RCSubscriptionInfoAPI.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -980,6 +990,7 @@ 2D4C617C2C5AD31A00A29FD2 /* RCStoreKitVersionAPI.m in Sources */, 2D4C61902C5AD31A00A29FD2 /* main.m in Sources */, 2D4C61932C5AD31A00A29FD2 /* RCPurchasesAPI.m in Sources */, + 35370AC82CFF8317004F0A64 /* RCSubscriptionInfoAPI.m in Sources */, 2D4C618C2C5AD31A00A29FD2 /* RCPurchasesDiagnosticsAPI.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -995,6 +1006,7 @@ 2D4C61DA2C5AD62A00A29FD2 /* RefundRequestStatusAPI.swift in Sources */, 2D4C61F52C5AD62A00A29FD2 /* PromotionalOfferAPI.swift in Sources */, 2D4C61E92C5AD62A00A29FD2 /* StoreProductAPI.swift in Sources */, + 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */, 2D4C61EA2C5AD62A00A29FD2 /* StorefrontAPI.swift in Sources */, 2D4C61E42C5AD62A00A29FD2 /* main.swift in Sources */, 2D4C61E62C5AD62A00A29FD2 /* StoreProductDiscountAPI.swift in Sources */, diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h new file mode 100644 index 0000000000..a87eafb710 --- /dev/null +++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h @@ -0,0 +1,19 @@ +// +// RCSubscriptionInfoAPI.h +// AllAPITests +// +// Created by Cesar de la Vega on 3/12/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCSubscriptionInfoAPI : NSObject + ++ (void)checkAPI; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m new file mode 100644 index 0000000000..e165b18d11 --- /dev/null +++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m @@ -0,0 +1,34 @@ +// +// RCSubscriptionInfoAPI.m +// AllAPITests +// +// Created by Cesar de la Vega on 3/12/24. +// + +#import "RCSubscriptionInfoAPI.h" + +@import RevenueCat; + +@implementation RCSubscriptionInfoAPI + ++ (void)checkAPI { + RCSubscriptionInfo *subscription; + + NSString *productIdentifier __unused = subscription.productIdentifier; + NSDate *purchaseDate __unused = subscription.purchaseDate; + NSDate *originalPurchaseDate __unused = subscription.originalPurchaseDate; + NSDate *expiresDate __unused = subscription.expiresDate; + RCStore store __unused = subscription.store; + BOOL isSandbox __unused = subscription.isSandbox; + NSDate *unsubscribeDetectedAt __unused = subscription.unsubscribeDetectedAt; + NSDate *billingIssuesDetectedAt __unused = subscription.billingIssuesDetectedAt; + NSDate *gracePeriodExpiresDate __unused = subscription.gracePeriodExpiresDate; + RCPurchaseOwnershipType ownershipType __unused = subscription.ownershipType; + RCPeriodType periodType __unused = subscription.periodType; + NSDate *refundedAt __unused = subscription.refundedAt; + NSString *storeTransactionId __unused = subscription.storeTransactionId; + BOOL isActive __unused = subscription.isActive; + BOOL willRenew __unused = subscription.willRenew; +} + +@end \ No newline at end of file diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCTransactionAPI.m b/Tests/APITesters/AllAPITests/ObjcAPITester/RCTransactionAPI.m index f93e38dc72..875ac8dea0 100644 --- a/Tests/APITesters/AllAPITests/ObjcAPITester/RCTransactionAPI.m +++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCTransactionAPI.m @@ -17,7 +17,8 @@ + (void)checkAPI { NSString *rci = rct.transactionIdentifier; NSString *pid = rct.productIdentifier; NSDate *date = rct.purchaseDate; - NSLog(rct, rci, pid, date); + NSString * _Nullable jws = rct.jwsRepresentation; + NSLog(rct, rci, pid, date, jws); } @end diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift index de2811be07..362f8dd90a 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift @@ -17,7 +17,9 @@ import RevenueCat var customerInfo: CustomerInfo! func checkCustomerInfoAPI() { let entitlementInfo: EntitlementInfos = customerInfo.entitlements + let asubsp: Set = customerInfo.activeSubscriptions let asubs: Set = customerInfo.activeSubscriptions + let appisp: Set = customerInfo.allPurchasedProductIdentifiers let appis: Set = customerInfo.allPurchasedProductIdentifiers let led: Date? = customerInfo.latestExpirationDate @@ -40,8 +42,12 @@ func checkCustomerInfoAPI() { let _: String = customerInfo.id + let subs: [String: SubscriptionInfo] = customerInfo.subscriptionsByProductIdentifier + + let subsp: [ProductIdentifier: SubscriptionInfo] = customerInfo.subscriptionsByProductIdentifier + print(customerInfo!, entitlementInfo, asubs, appis, led!, nst, oav!, opd!, rDate!, fSeen, - oaud!, murl!, edfpi!, pdfpi!, exdf!, pdfe!, desc, rawData) + oaud!, murl!, edfpi!, pdfpi!, exdf!, pdfe!, desc, rawData, subs) } func checkCacheFetchPolicyEnum(_ policy: CacheFetchPolicy) { diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift new file mode 100644 index 0000000000..829d5e62b7 --- /dev/null +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.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 +// +// SubscriptionInfoAPI.swift +// +// Created by Cesar de la Vega on 26/11/24. + +import Foundation +import RevenueCat + +var subscription: SubscriptionInfo! +func checkSubscriptionInfoAPI() { + let pId: String = subscription.productIdentifier + let pIdP: ProductIdentifier = subscription.productIdentifier + let pd: Date = subscription.purchaseDate + let opd: Date? = subscription.originalPurchaseDate + let eDate: Date? = subscription.expiresDate + let store: Store = subscription.store + let iss: Bool = subscription.isSandbox + let uda: Date? = subscription.unsubscribeDetectedAt + let bida: Date? = subscription.billingIssuesDetectedAt + let gped: Date? = subscription.gracePeriodExpiresDate + let oType: PurchaseOwnershipType = subscription.ownershipType + let pType: PeriodType = subscription.periodType + let rAt: Date? = subscription.refundedAt + let txId: String? = subscription.storeTransactionId + let isActive: Bool = subscription.isActive + let willRenew: Bool = subscription.willRenew +} diff --git a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift index 8b57577b51..d7d0c2b80a 100644 --- a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift +++ b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift @@ -238,7 +238,7 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": [:] as [String: Any] + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "\(mockEntitlementID)": [ @@ -260,8 +260,8 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": [:] as [String: Any], - "onemonth_freetrial2": [:] as [String: Any] + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + "onemonth_freetrial2": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "\(mockEntitlementID)": [ @@ -287,7 +287,9 @@ private extension BeginRefundRequestHelperTests { "original_application_version": "2083", "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], - "subscriptions": [:] as [String: Any], + "subscriptions": [ + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] + ], "entitlements": [ "\(mockEntitlementID)": [ "expires_date": "2000-08-30T02:40:36Z", @@ -307,7 +309,9 @@ private extension BeginRefundRequestHelperTests { "original_application_version": "2083", "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], - "subscriptions": [:] as [String: Any], + "subscriptions": [ + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] + ], "entitlements": [ "pro": [ "expires_date": "2100-08-30T02:40:36Z", diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 3bf18c548f..396e662524 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -58,8 +58,19 @@ 88DFC1932BCF490400273B6D /* OfferingsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DFC1922BCF490400273B6D /* OfferingsResponse.swift */; }; 88DFC1942BCF490400273B6D /* PaywallsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DFC1912BCF490400273B6D /* PaywallsResponse.swift */; }; 88DFC1972BCF4A5100273B6D /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DFC1952BCF4A4300273B6D /* MockData.swift */; }; + FA29FBB22CCAA7A500DA1976 /* SnapshottingTests in Frameworks */ = {isa = PBXBuildFile; productRef = FA29FBB12CCAA7A500DA1976 /* SnapshottingTests */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + FA29FBAC2CCAA79800DA1976 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4F6BED922A26A64200CD9322 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F6BED992A26A64200CD9322; + remoteInfo = PaywallsTester; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 4F6BEDD72A26A68E00CD9322 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -138,8 +149,13 @@ 88DFC1912BCF490400273B6D /* PaywallsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallsResponse.swift; sourceTree = ""; }; 88DFC1922BCF490400273B6D /* OfferingsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsResponse.swift; sourceTree = ""; }; 88DFC1952BCF4A4300273B6D /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; + FA29FBA82CCAA79800DA1976 /* PaywallsTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PaywallsTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + FA29FBA92CCAA79800DA1976 /* PaywallsTesterTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = PaywallsTesterTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 4F6BED972A26A64200CD9322 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -151,6 +167,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FA29FBA52CCAA79800DA1976 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA29FBB22CCAA7A500DA1976 /* SnapshottingTests in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -170,6 +194,7 @@ 773C13462CB5865400219E42 /* Local.xcconfig */, 4F4EE7D02A5731CF00D7EAE1 /* purchases-ios */, 4FC046BB2A572E3700A28BCF /* PaywallsTester */, + FA29FBA92CCAA79800DA1976 /* PaywallsTesterTests */, 4F6BED9B2A26A64200CD9322 /* Products */, 4F6BEDCC2A26A68E00CD9322 /* Frameworks */, ); @@ -179,6 +204,7 @@ isa = PBXGroup; children = ( 4F6BED9A2A26A64200CD9322 /* PaywallsTester.app */, + FA29FBA82CCAA79800DA1976 /* PaywallsTesterTests.xctest */, ); name = Products; sourceTree = ""; @@ -371,6 +397,30 @@ productReference = 4F6BED9A2A26A64200CD9322 /* PaywallsTester.app */; productType = "com.apple.product-type.application"; }; + FA29FBA72CCAA79800DA1976 /* PaywallsTesterTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FA29FBAE2CCAA79800DA1976 /* Build configuration list for PBXNativeTarget "PaywallsTesterTests" */; + buildPhases = ( + FA29FBA42CCAA79800DA1976 /* Sources */, + FA29FBA52CCAA79800DA1976 /* Frameworks */, + FA29FBA62CCAA79800DA1976 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FA29FBAD2CCAA79800DA1976 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + FA29FBA92CCAA79800DA1976 /* PaywallsTesterTests */, + ); + name = PaywallsTesterTests; + packageProductDependencies = ( + FA29FBB12CCAA7A500DA1976 /* SnapshottingTests */, + ); + productName = PaywallsTesterTests; + productReference = FA29FBA82CCAA79800DA1976 /* PaywallsTesterTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -378,12 +428,16 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1430; TargetAttributes = { 4F6BED992A26A64200CD9322 = { CreatedOnToolsVersion = 14.3; }; + FA29FBA72CCAA79800DA1976 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 4F6BED992A26A64200CD9322; + }; }; }; buildConfigurationList = 4F6BED952A26A64200CD9322 /* Build configuration list for PBXProject "PaywallsTester" */; @@ -402,12 +456,14 @@ ); mainGroup = 4F6BED912A26A64200CD9322; packageReferences = ( + FA29FBA12CCAA76E00DA1976 /* XCRemoteSwiftPackageReference "SnapshotPreviews" */, ); productRefGroup = 4F6BED9B2A26A64200CD9322 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4F6BED992A26A64200CD9322 /* PaywallsTester */, + FA29FBA72CCAA79800DA1976 /* PaywallsTesterTests */, ); }; /* End PBXProject section */ @@ -423,6 +479,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FA29FBA62CCAA79800DA1976 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -478,8 +541,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FA29FBA42CCAA79800DA1976 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + FA29FBAD2CCAA79800DA1976 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F6BED992A26A64200CD9322 /* PaywallsTester */; + targetProxy = FA29FBAC2CCAA79800DA1976 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 4F4557E42A6FFE6A00160521 /* Localizable.strings */ = { isa = PBXVariantGroup; @@ -709,6 +787,54 @@ }; name = Release; }; + FA29FBAF2CCAA79800DA1976 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PaywallsTesterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaywallsTester.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaywallsTester"; + }; + name = Debug; + }; + FA29FBB02CCAA79800DA1976 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PaywallsTesterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaywallsTester.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaywallsTester"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -730,8 +856,28 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + FA29FBAE2CCAA79800DA1976 /* Build configuration list for PBXNativeTarget "PaywallsTesterTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA29FBAF2CCAA79800DA1976 /* Debug */, + FA29FBB02CCAA79800DA1976 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + FA29FBA12CCAA76E00DA1976 /* XCRemoteSwiftPackageReference "SnapshotPreviews" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/EmergeTools/SnapshotPreviews"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.10.16; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 4F4EE7D12A5731E800D7EAE1 /* RevenueCat */ = { isa = XCSwiftPackageProductDependency; @@ -741,6 +887,11 @@ isa = XCSwiftPackageProductDependency; productName = RevenueCatUI; }; + FA29FBB12CCAA7A500DA1976 /* SnapshottingTests */ = { + isa = XCSwiftPackageProductDependency; + package = FA29FBA12CCAA76E00DA1976 /* XCRemoteSwiftPackageReference "SnapshotPreviews" */; + productName = SnapshottingTests; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4F6BED922A26A64200CD9322 /* Project object */; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme index 0029172252..45215ff4a6 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme @@ -64,6 +64,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + + + + + + Date { - return try XCTUnwrap(Calendar.current.date(byAdding: .day, value: days, to: Date())) - } - - private static let expiredSubscriptionDate = ISO8601DateFormatter.default.string( - // swiftlint:disable:next force_try - from: try! BasicCustomerInfoTests.date(withDaysAgo: -1) - ) static let validSubscriberResponse: [String: Any] = [ "request_date": "2018-10-19T02:40:36Z", "request_date_ms": Int64(1563379533946), @@ -51,6 +43,7 @@ class BasicCustomerInfoTests: TestCase { ] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal", "is_sandbox": false @@ -63,6 +56,7 @@ class BasicCustomerInfoTests: TestCase { "purchase_date": "2018-05-20T06:24:50Z" ], "onemonth": [ + "purchase_date": "2000-07-30T02:40:36Z", "expires_date": BasicCustomerInfoTests.expiredSubscriptionDate, "period_type": "normal", "is_sandbox": false @@ -103,17 +97,29 @@ class BasicCustomerInfoTests: TestCase { ] as [String: Any] ] - static let validTwoProductsJSON = "{" + - "\"request_date\": \"2018-05-20T06:24:50Z\"," + - "\"subscriber\": {" + - "\"first_seen\": \"2018-05-20T06:24:50Z\"," + - "\"original_application_version\": \"1.0\"," + - "\"original_app_user_id\": \"abcd\"," + - "\"other_purchases\": {}," + - "\"subscriptions\":{" + - "\"product_a\": {\"expires_date\": \"2018-05-27T06:24:50Z\",\"period_type\": \"normal\"}," + - "\"product_b\": {\"expires_date\": \"2018-05-27T05:24:50Z\",\"period_type\": \"normal\"}" + - "}}}" + static let validTwoProductsJSON = """ + { + "request_date": "2018-05-20T06:24:50Z", + "subscriber": { + "first_seen": "2018-05-20T06:24:50Z", + "original_application_version": "1.0", + "original_app_user_id": "abcd", + "other_purchases": {}, + "subscriptions": { + "product_a": { + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + }, + "product_b": { + "purchase_date": "2018-04-27T05:24:50Z", + "expires_date": "2018-05-27T05:24:50Z", + "period_type": "normal" + } + } + } + } + """ private var customerInfo: CustomerInfo! @@ -397,18 +403,22 @@ class BasicCustomerInfoTests: TestCase { ], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1989-08-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z", "period_type": "normal" ], "pro.1": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "pro.2": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z", "period_type": "normal" ] @@ -468,9 +478,11 @@ class BasicCustomerInfoTests: TestCase { ], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z" ], "threemonth_freetrial": [ + "purchase_date": "1990-08-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -547,6 +559,7 @@ class BasicCustomerInfoTests: TestCase { "original_app_user_id": "", "subscriptions": [ "pro.1": [ + "purchase_date": "2018-07-30T02:40:36Z", "expires_date": "2018-12-19T02:40:36Z" ]], "other_purchases": [:] as [String: Any], @@ -565,6 +578,7 @@ class BasicCustomerInfoTests: TestCase { "original_app_user_id": "", "subscriptions": [ "pro.1": [ + "purchase_date": "2018-07-30T02:40:36Z", "expires_date": "2018-12-19T02:40:36Z" ] ], @@ -784,13 +798,16 @@ class BasicCustomerInfoTests: TestCase { "non_subscriptions": [:] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "twomonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -828,13 +845,16 @@ class BasicCustomerInfoTests: TestCase { "non_subscriptions": [:] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "twomonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -915,9 +935,35 @@ class BasicCustomerInfoTests: TestCase { expect(self.customerInfo.copy(with: .verifiedOnDevice).isComputedOffline) == true } - // MARK: - Private +} + +extension CustomerInfo { - private func verifyCopy( + convenience init?(testData: [String: Any]) { + do { + try self.init(data: testData) + } catch { + let errorDescription = (error as? DescribableError)?.description ?? error.localizedDescription + Logger.error("Caught error creating testData, this is probably expected, right? \(errorDescription).") + + return nil + } + } + +} + +private extension BasicCustomerInfoTests { + + static func date(withDaysAgo days: Int) throws -> Date { + return try XCTUnwrap(Calendar.current.date(byAdding: .day, value: days, to: Date())) + } + + static let expiredSubscriptionDate = ISO8601DateFormatter.default.string( + // swiftlint:disable:next force_try + from: try! BasicCustomerInfoTests.date(withDaysAgo: -1) + ) + + func verifyCopy( of customerInfo: CustomerInfo, onlyModifiesEntitlementVerification newVerification: VerificationResult ) { @@ -930,7 +976,7 @@ class BasicCustomerInfoTests: TestCase { expect(copyWithOriginalVerification) == customerInfo } - private func verifyCopy( + func verifyCopy( of customerInfo: CustomerInfo, onlyModifiesRequestDate newRequestDate: Date ) { @@ -946,21 +992,6 @@ class BasicCustomerInfoTests: TestCase { } -extension CustomerInfo { - - convenience init?(testData: [String: Any]) { - do { - try self.init(data: testData) - } catch { - let errorDescription = (error as? DescribableError)?.description ?? error.localizedDescription - Logger.error("Caught error creating testData, this is probably expected, right? \(errorDescription).") - - return nil - } - } - -} - private extension BasicCustomerInfoTests { static let sampleTestDataWithEntitlements: [String: Any] = [ diff --git a/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift index 0b57c289a2..9a63e90000 100644 --- a/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift @@ -41,6 +41,7 @@ class BackendSubscriberAttributesTests: TestCase { "original_app_user_id": "app_user_id", "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2017-07-30T02:40:36Z", "expires_date": "2017-08-30T02:40:36Z" ] ] diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7ce8fb4609..be150d1d7b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -130,6 +130,24 @@ platform :ios do end end + desc "Build the Paywalls Test app for Emerge Snapshots" + lane :build_paywalls_tester_for_emerge do |options| + File.write("Local.xcconfig", "SWIFT_ACTIVE_COMPILATION_CONDITIONS = \$(inherited) PAYWALL_COMPONENTS") + replace_text_in_files( + previous_text: "\/\/ REPLACE_WITH_DEFINES_HERE", + new_text: ".define(\"PAYWALL_COMPONENTS\")", + paths_of_files_to_update: [ + './Package.swift', + ] + ) + scan( + workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace", + scheme: "PaywallsTester - Live Config", + build_for_testing: true, + ) + emerge(repo_name: "RevenueCat/purchases-ios") + end + desc "Runs all the iOS tests" lane :test_ios do |options| generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true" @@ -814,7 +832,7 @@ platform :ios do purchase_tester_root = "#{project_root}/Tests/TestingApps/PurchaseTesterSwiftUI" xcodeproj_path = "#{purchase_tester_root}/PurchaseTester.xcodeproj" - # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory, + # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory, # like the Matchfile. So we move to that project's directory to run them. Dir.chdir("#{purchase_tester_root}/fastlane") do match(readonly: true, platform: "ios") @@ -823,7 +841,7 @@ platform :ios do xcodeproj: xcodeproj_path, ) end - + build_ios_app( workspace: 'RevenueCat.xcworkspace', scheme: "PurchaseTester", @@ -848,7 +866,7 @@ platform :ios do UI.message("Dry run mode enabled. Skipping upload to TestFlight") end - # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory, + # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory, # like the Matchfile. So we move to that project's directory to run them. Dir.chdir("#{purchase_tester_root}/fastlane") do match(readonly: true, platform: "macos", additional_cert_types: "mac_installer_distribution") @@ -881,7 +899,7 @@ platform :ios do else UI.message("Dry run mode enabled. Skipping upload to TestFlight") end - + end desc "Clones or updates snapshots repo" @@ -919,7 +937,7 @@ platform :ios do lane :deploy_to_spm do source_repo = ENV["CIRCLE_REPOSITORY_URL"] || "https://github.com/revenuecat/#{repo_name}" destination_repo = source_repo.sub(/\.git\z/, "-spm.git") - + git_clone_and_push( source_repo: source_repo, destination_repo: destination_repo @@ -961,7 +979,7 @@ platform :ios do rescue => ex UI.error("Failed to enable customer center. Error while applying changes from commit #{commit_hash}") UI.error(ex.message) - + if UI.confirm("Do you want to abort the customer center enablement?") sh("git", "cherry-pick", "--abort") UI.message("Customer center enablement aborted") diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 35980c512f..9f0eb0bfa0 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -4,3 +4,4 @@ gem 'fastlane-plugin-create_xcframework', git: "https://github.com/RevenueCat/fastlane-plugin-create_xcframework" gem "fastlane-plugin-revenuecat_internal", git: "https://github.com/RevenueCat/fastlane-plugin-revenuecat_internal" +gem 'fastlane-plugin-emerge' diff --git a/fastlane/README.md b/fastlane/README.md index 6434e6c33e..08a1b33109 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -74,6 +74,14 @@ Creates PR changing version to next minor adding a -SNAPSHOT suffix Setup development environment +### ios build_paywalls_test + +```sh +[bundle exec] fastlane ios build_paywalls_test +``` + +Builds the Paywalls Test app + ### ios test_ios ```sh