diff --git a/.circleci/config.yml b/.circleci/config.yml index b795e6c218..2de8871961 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,6 +19,8 @@ release-tags: &release-tags branches: ignore: /.*/ +orbs: + macos: circleci/macos@dev:25cb9bd version: 2.1 commands: @@ -55,12 +57,10 @@ commands: - run: name: Replace API key command: bundle exec fastlane replace_api_key_integration_tests - - run: name: Run tests working_directory: << parameters.directory >> - command: bundle exec fastlane scan - + command: bundle exec fastlane test - store_test_results: path: fastlane/test_output - store_artifacts: @@ -112,7 +112,7 @@ commands: jobs: runtest: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -123,16 +123,12 @@ jobs: - run: name: Run tests command: bundle exec fastlane test - environment: - SCAN_SCHEME: RevenueCat - - store_test_results: - path: fastlane/test_output - store_artifacts: path: fastlane/test_output/xctest destination: scan-test-output buildTvWatchAndMacOS: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -142,12 +138,12 @@ jobs: command: brew install swiftlint - install-dependencies - run: - name: Build AppleTV, WatchOS, and macOS + name: Build tvOS, watchOS and macOS command: bundle exec fastlane build_tv_watch_mac swift_apitest: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -155,11 +151,12 @@ jobs: - update-spm-integration-commit - run: name: Build SwiftAPITester - command: xcodebuild -workspace APITesters/APITesters.xcworkspace -scheme SwiftAPITester + command: | + bundle exec fastlane build_swift_api_tester objc_apitest: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -167,26 +164,23 @@ jobs: - update-spm-integration-commit - run: name: Build ObjCAPITester - command: xcodebuild -workspace APITesters/APITesters.xcworkspace -scheme ObjCAPITester + command: | + bundle exec fastlane build_objc_api_tester - storekit_tests: + backend_integration_tests: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: - checkout - - run: - name: Open simulator - command: xcrun instruments -w "iPhone 11 Pro (14.5) [" || true - - install-dependencies - run: - name: Run StoreKit Tests - command: bundle exec fastlane storekit_tests + name: Run backend_integration Tests + command: bundle exec fastlane backend_integration_tests environment: - SCAN_DEVICE: iPhone 11 Pro (14.5) + SCAN_DEVICE: iPhone 11 Pro (15.0) - store_test_results: path: fastlane/test_output - store_artifacts: @@ -196,7 +190,7 @@ jobs: release-checks: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -228,7 +222,7 @@ jobs: docs-deploy: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios shell: /bin/bash --login -o pipefail steps: @@ -252,7 +246,7 @@ jobs: make-release: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -265,7 +259,7 @@ jobs: prepare-next-version: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -278,7 +272,7 @@ jobs: integration-tests-cocoapods: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -299,7 +293,7 @@ jobs: integration-tests-swift-package-manager: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -311,7 +305,7 @@ jobs: integration-tests-carthage: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -341,7 +335,7 @@ jobs: integration-tests-xcode-direct-integration: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -352,7 +346,7 @@ jobs: lint: macos: - xcode: "12.5.1" + xcode: "13.1.0" working_directory: ~/purchases-ios/ shell: /bin/bash --login -o pipefail steps: @@ -379,8 +373,8 @@ workflows: - swift_apitest - objc_apitest - buildTvWatchAndMacOS - - storekit_tests: *release-tags-and-branches - release-checks: *release-branches + - backend_integration_tests - integration-tests-cocoapods: *release-tags-and-branches - integration-tests-swift-package-manager: *release-tags-and-branches - integration-tests-carthage: *release-tags-and-branches diff --git a/.swiftlint.yml b/.swiftlint.yml index ee6a3d7c90..1468301ae2 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,7 +4,7 @@ excluded: - PurchasesTests - Examples - IntegrationTests - - StoreKitTests + - BackendIntegrationTests - PublicSDKAPITester - vendor - scan_derived_data diff --git a/APITesters/APITesters.xcworkspace/xcshareddata/swiftpm/Package.resolved b/APITesters/APITesters.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5d7c1622bd..657da3fad6 100644 --- a/APITesters/APITesters.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/APITesters/APITesters.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "repositoryURL": "https://github.com/RevenueCat/purchases-ios.git", "state": { "branch": "main", - "revision": "1058fdbf011bf4d040f2a961149bc564b97e63e9", + "revision": "8699a69f7e8eaedbb888639d0ddca2bc537f450d", "version": null } } diff --git a/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj b/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj index 0bfa7967d0..8889fd1b54 100644 --- a/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj +++ b/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj @@ -7,6 +7,28 @@ objects = { /* Begin PBXBuildFile section */ + 2DD778F8270E235B0079CBD4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778F7270E235B0079CBD4 /* AppDelegate.swift */; }; + 2DD778FA270E235B0079CBD4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778F9270E235B0079CBD4 /* SceneDelegate.swift */; }; + 2DD778FC270E235B0079CBD4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778FB270E235B0079CBD4 /* ViewController.swift */; }; + 2DD778FF270E235B0079CBD4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DD778FD270E235B0079CBD4 /* Main.storyboard */; }; + 2DD77901270E235C0079CBD4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DD77900270E235C0079CBD4 /* Assets.xcassets */; }; + 2DD77904270E235C0079CBD4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DD77902270E235C0079CBD4 /* LaunchScreen.storyboard */; }; + 2DD77909270E23870079CBD4 /* RCAttributionNetworkAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EC26EBE84F007DDB75 /* RCAttributionNetworkAPI.m */; }; + 2DD7790A270E23870079CBD4 /* RCBackendErrorCodeAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F726EBE84F007DDB75 /* RCBackendErrorCodeAPI.m */; }; + 2DD7790B270E23870079CBD4 /* RCCustomerInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F026EBE84F007DDB75 /* RCCustomerInfoAPI.m */; }; + 2DD7790C270E23870079CBD4 /* RCPurchasesErrorCodeAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EE26EBE84F007DDB75 /* RCPurchasesErrorCodeAPI.m */; }; + 2DD7790E270E23870079CBD4 /* RCIntroEligibilityAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F226EBE84F007DDB75 /* RCIntroEligibilityAPI.m */; }; + 2DD7790F270E23870079CBD4 /* RCPurchasesAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614ED26EBE84F007DDB75 /* RCPurchasesAPI.m */; }; + 2DD77910270E23870079CBD4 /* RCPurchasesErrorUtilsAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614E026EBE84F007DDB75 /* RCPurchasesErrorUtilsAPI.m */; }; + 2DD77911270E23870079CBD4 /* RCEntitlementInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F826EBE84F007DDB75 /* RCEntitlementInfoAPI.m */; }; + 2DD77912270E23870079CBD4 /* RCOfferingsAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DD26EBE84E007DDB75 /* RCOfferingsAPI.m */; }; + 2DD77913270E23870079CBD4 /* RCEntitlementInfosAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614E926EBE84F007DDB75 /* RCEntitlementInfosAPI.m */; }; + 2DD77914270E23870079CBD4 /* RCOfferingAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DF26EBE84F007DDB75 /* RCOfferingAPI.m */; }; + 2DD77915270E23870079CBD4 /* RCPackageAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DE26EBE84F007DDB75 /* RCPackageAPI.m */; }; + 2DD77916270E23870079CBD4 /* RCTransactionAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F426EBE84F007DDB75 /* RCTransactionAPI.m */; }; + 2DD77919270E23CC0079CBD4 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 2DD77918270E23CC0079CBD4 /* RevenueCat */; }; + A513AD31272B327A00E0C1BA /* RCRefundRequestStatusAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */; }; + A513AD32272B328800E0C1BA /* RCRefundRequestStatusAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */; }; A519264D26EBCEF6007D0BD1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A519264C26EBCEF6007D0BD1 /* main.m */; }; A52A8A3C26EC2AEC00F06846 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = A52A8A3B26EC2AEC00F06846 /* RevenueCat */; }; A5D614F926EBE84F007DDB75 /* RCOfferingsAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DD26EBE84E007DDB75 /* RCOfferingsAPI.m */; }; @@ -35,6 +57,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2DD778F5270E235B0079CBD4 /* ObjCAPITesterIOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ObjCAPITesterIOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2DD778F7270E235B0079CBD4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2DD778F9270E235B0079CBD4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2DD778FB270E235B0079CBD4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 2DD778FE270E235B0079CBD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 2DD77900270E235C0079CBD4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2DD77903270E235C0079CBD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2DD77905270E235C0079CBD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A513AD2F272B327A00E0C1BA /* RCRefundRequestStatusAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCRefundRequestStatusAPI.h; sourceTree = ""; }; + A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCRefundRequestStatusAPI.m; sourceTree = ""; }; A519264926EBCEF6007D0BD1 /* ObjCAPITester */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = ObjCAPITester; sourceTree = BUILT_PRODUCTS_DIR; }; A519264C26EBCEF6007D0BD1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; A5D614DD26EBE84E007DDB75 /* RCOfferingsAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCOfferingsAPI.m; sourceTree = ""; }; @@ -62,6 +94,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2DD778F2270E235B0079CBD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DD77919270E23CC0079CBD4 /* RevenueCat in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A519264626EBCEF6007D0BD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -73,10 +113,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2DD778F6270E235B0079CBD4 /* ObjCAPITesterIOS */ = { + isa = PBXGroup; + children = ( + 2DD778F7270E235B0079CBD4 /* AppDelegate.swift */, + 2DD778F9270E235B0079CBD4 /* SceneDelegate.swift */, + 2DD778FB270E235B0079CBD4 /* ViewController.swift */, + 2DD778FD270E235B0079CBD4 /* Main.storyboard */, + 2DD77900270E235C0079CBD4 /* Assets.xcassets */, + 2DD77902270E235C0079CBD4 /* LaunchScreen.storyboard */, + 2DD77905270E235C0079CBD4 /* Info.plist */, + ); + path = ObjCAPITesterIOS; + sourceTree = ""; + }; A519264026EBCEF6007D0BD1 = { isa = PBXGroup; children = ( A519264B26EBCEF6007D0BD1 /* ObjCAPITester */, + 2DD778F6270E235B0079CBD4 /* ObjCAPITesterIOS */, A519264A26EBCEF6007D0BD1 /* Products */, A52A8A3A26EC2AEC00F06846 /* Frameworks */, ); @@ -86,6 +141,7 @@ isa = PBXGroup; children = ( A519264926EBCEF6007D0BD1 /* ObjCAPITester */, + 2DD778F5270E235B0079CBD4 /* ObjCAPITesterIOS.app */, ); name = Products; sourceTree = ""; @@ -116,6 +172,8 @@ A5D614E726EBE84F007DDB75 /* RCTransactionAPI.h */, A5D614F426EBE84F007DDB75 /* RCTransactionAPI.m */, A519264C26EBCEF6007D0BD1 /* main.m */, + A513AD2F272B327A00E0C1BA /* RCRefundRequestStatusAPI.h */, + A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */, ); path = ObjCAPITester; sourceTree = ""; @@ -130,6 +188,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2DD778F4270E235B0079CBD4 /* ObjCAPITesterIOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2DD77908270E235C0079CBD4 /* Build configuration list for PBXNativeTarget "ObjCAPITesterIOS" */; + buildPhases = ( + 2DD778F1270E235B0079CBD4 /* Sources */, + 2DD778F2270E235B0079CBD4 /* Frameworks */, + 2DD778F3270E235B0079CBD4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ObjCAPITesterIOS; + packageProductDependencies = ( + 2DD77918270E23CC0079CBD4 /* RevenueCat */, + ); + productName = ObjCAPITesterIOS; + productReference = 2DD778F5270E235B0079CBD4 /* ObjCAPITesterIOS.app */; + productType = "com.apple.product-type.application"; + }; A519264826EBCEF6007D0BD1 /* ObjCAPITester */ = { isa = PBXNativeTarget; buildConfigurationList = A519265026EBCEF6007D0BD1 /* Build configuration list for PBXNativeTarget "ObjCAPITester" */; @@ -157,8 +235,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1300; TargetAttributes = { + 2DD778F4270E235B0079CBD4 = { + CreatedOnToolsVersion = 13.0; + }; A519264826EBCEF6007D0BD1 = { CreatedOnToolsVersion = 13.0; LastSwiftMigration = 1300; @@ -182,11 +264,49 @@ projectRoot = ""; targets = ( A519264826EBCEF6007D0BD1 /* ObjCAPITester */, + 2DD778F4270E235B0079CBD4 /* ObjCAPITesterIOS */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 2DD778F3270E235B0079CBD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DD77904270E235C0079CBD4 /* LaunchScreen.storyboard in Resources */, + 2DD77901270E235C0079CBD4 /* Assets.xcassets in Resources */, + 2DD778FF270E235B0079CBD4 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ + 2DD778F1270E235B0079CBD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DD778FC270E235B0079CBD4 /* ViewController.swift in Sources */, + 2DD778F8270E235B0079CBD4 /* AppDelegate.swift in Sources */, + 2DD77914270E23870079CBD4 /* RCOfferingAPI.m in Sources */, + A513AD32272B328800E0C1BA /* RCRefundRequestStatusAPI.m in Sources */, + 2DD7790C270E23870079CBD4 /* RCPurchasesErrorCodeAPI.m in Sources */, + 2DD77915270E23870079CBD4 /* RCPackageAPI.m in Sources */, + 2DD7790A270E23870079CBD4 /* RCBackendErrorCodeAPI.m in Sources */, + 2DD77909270E23870079CBD4 /* RCAttributionNetworkAPI.m in Sources */, + 2DD778FA270E235B0079CBD4 /* SceneDelegate.swift in Sources */, + 2DD7790E270E23870079CBD4 /* RCIntroEligibilityAPI.m in Sources */, + 2DD77916270E23870079CBD4 /* RCTransactionAPI.m in Sources */, + 2DD7790F270E23870079CBD4 /* RCPurchasesAPI.m in Sources */, + 2DD77911270E23870079CBD4 /* RCEntitlementInfoAPI.m in Sources */, + 2DD77912270E23870079CBD4 /* RCOfferingsAPI.m in Sources */, + 2DD77910270E23870079CBD4 /* RCPurchasesErrorUtilsAPI.m in Sources */, + 2DD7790B270E23870079CBD4 /* RCCustomerInfoAPI.m in Sources */, + 2DD77913270E23870079CBD4 /* RCEntitlementInfosAPI.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A519264526EBCEF6007D0BD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -197,6 +317,7 @@ A5D614FB26EBE84F007DDB75 /* RCOfferingAPI.m in Sources */, A5D614FF26EBE84F007DDB75 /* RCAttributionNetworkAPI.m in Sources */, A5D6150626EBE84F007DDB75 /* RCEntitlementInfoAPI.m in Sources */, + A513AD31272B327A00E0C1BA /* RCRefundRequestStatusAPI.m in Sources */, A5D614FA26EBE84F007DDB75 /* RCPackageAPI.m in Sources */, A5D6150126EBE84F007DDB75 /* RCPurchasesErrorCodeAPI.m in Sources */, A5D614F926EBE84F007DDB75 /* RCOfferingsAPI.m in Sources */, @@ -208,7 +329,95 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 2DD778FD270E235B0079CBD4 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DD778FE270E235B0079CBD4 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 2DD77902270E235C0079CBD4 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DD77903270E235C0079CBD4 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + 2DD77906270E235C0079CBD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8SXR2327BM; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ObjCAPITesterIOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.ObjCAPITesterIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development com.revenuecat.ObjCAPITesterIOS"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2DD77907270E235C0079CBD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8SXR2327BM; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ObjCAPITesterIOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.sampleapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development com.revenuecat.sampleapp"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; A519264E26EBCEF6007D0BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -360,6 +569,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2DD77908270E235C0079CBD4 /* Build configuration list for PBXNativeTarget "ObjCAPITesterIOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2DD77906270E235C0079CBD4 /* Debug */, + 2DD77907270E235C0079CBD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; A519264426EBCEF6007D0BD1 /* Build configuration list for PBXProject "ObjCAPITester" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -392,6 +610,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 2DD77918270E23CC0079CBD4 /* RevenueCat */ = { + isa = XCSwiftPackageProductDependency; + package = A519265326EBCFA6007D0BD1 /* XCRemoteSwiftPackageReference "purchases-ios" */; + productName = RevenueCat; + }; A52A8A3B26EC2AEC00F06846 /* RevenueCat */ = { isa = XCSwiftPackageProductDependency; package = A519265326EBCFA6007D0BD1 /* XCRemoteSwiftPackageReference "purchases-ios" */; diff --git a/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/xcshareddata/xcschemes/ObjCAPITesterIOS.xcscheme b/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/xcshareddata/xcschemes/ObjCAPITesterIOS.xcscheme new file mode 100644 index 0000000000..feaf4a4679 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/xcshareddata/xcschemes/ObjCAPITesterIOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APITesters/ObjCAPITester/ObjCAPITester/RCPackageAPI.m b/APITesters/ObjCAPITester/ObjCAPITester/RCPackageAPI.m index c0d1195cbc..9fc5163a74 100644 --- a/APITesters/ObjCAPITester/ObjCAPITester/RCPackageAPI.m +++ b/APITesters/ObjCAPITester/ObjCAPITester/RCPackageAPI.m @@ -20,13 +20,13 @@ + (void)checkAPI { RCPackage *p; + RCProductDetails *pw = p.productDetails; NSString *i = p.identifier; RCPackageType t = p.packageType; - SKProduct *prod = p.product; NSString *lps = p.localizedPriceString; NSString *lips = p.localizedIntroductoryPriceString; - NSLog(p, i, t, prod, lps, lips); + NSLog(p, pw, i, t, lps, lips); } + (void)checkEnums { diff --git a/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 53f33a2911..0565cb0330 100644 --- a/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -30,8 +30,6 @@ NSString *appUserID; BOOL isAnonymous; + (void)checkAPI { - // TODO: iOS ONLY, TEST FOR THIS API BY LOOKING UP SELECTOR - // [p presentCodeRedemptionSheet]; RCPurchases *p = [RCPurchases configureWithAPIKey:@""]; [RCPurchases configureWithAPIKey:@"" appUserID:@""]; [RCPurchases configureWithAPIKey:@"" appUserID:nil]; @@ -147,6 +145,14 @@ shouldPurchasePromoProduct:skp RCCustomerInfo * _Nullable info, NSError * _Nullable error, BOOL cancelled)) {}]; + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + [p beginRefundRequestFor:@"1234" completion:^(RCRefundRequestStatus s, NSError * _Nullable e) { }]; +#endif + +#if TARGET_OS_IPHONE + [p presentCodeRedemptionSheet]; +#endif } + (void)checkEnums { diff --git a/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m b/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m index 6653a9f9ad..3b8d9e421b 100644 --- a/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m +++ b/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m @@ -50,8 +50,11 @@ case RCMissingAppUserIDForAliasCreationError: case RCProductDiscountMissingSubscriptionGroupIdentifierError: case RCConfigurationError: + case RCUnsupportedError: + case RCCustomerInfoError: + case RCSystemInfoError: + case RCBeginRefundRequestError: NSLog(@"%ld", (long)errCode); - } } diff --git a/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.h b/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.h new file mode 100644 index 0000000000..46f346bee2 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.h @@ -0,0 +1,18 @@ +// +// RCRefundRequestStatusAPI.h +// ObjCAPITester +// +// Created by Madeline Beyl on 10/28/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCRefundRequestStatusAPI : NSObject + ++ (void)checkEnums; + +@end + +NS_ASSUME_NONNULL_END diff --git a/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.m b/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.m new file mode 100644 index 0000000000..71edbdb27d --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITester/RCRefundRequestStatusAPI.m @@ -0,0 +1,23 @@ +// +// RCRefundRequestStatusAPI.m +// ObjCAPITester +// +// Created by Madeline Beyl on 10/28/21. +// + +#import "RCRefundRequestStatusAPI.h" +@import RevenueCat; + +@implementation RCRefundRequestStatusAPI + ++ (void) checkEnums { + RCRefundRequestStatus status = RCRefundRequestSuccess; + switch(status) { + case RCRefundRequestSuccess: + case RCRefundRequestError: + case RCRefundRequestUserCancelled: + NSLog(@"%ld", (long)status); + } +} + +@end diff --git a/APITesters/ObjCAPITester/ObjCAPITester/main.m b/APITesters/ObjCAPITester/ObjCAPITester/main.m index 78c1a614a9..a9ed5d1896 100644 --- a/APITesters/ObjCAPITester/ObjCAPITester/main.m +++ b/APITesters/ObjCAPITester/ObjCAPITester/main.m @@ -15,8 +15,9 @@ #import "RCCustomerInfoAPI.h" #import "RCPurchasesAPI.h" #import "RCPurchasesErrorCodeAPI.h" -#import "RCTransactionAPI.h" #import "RCPackageAPI.h" +#import "RCRefundRequestStatusAPI.h" +#import "RCTransactionAPI.h" @import StoreKit; @@ -47,6 +48,8 @@ int main(int argc, const char * argv[]) { [RCPurchasesErrorCodeAPI checkEnums]; + [RCRefundRequestStatusAPI checkEnums]; + [RCTransactionAPI checkAPI]; } return 0; diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/AppDelegate.swift b/APITesters/ObjCAPITester/ObjCAPITesterIOS/AppDelegate.swift new file mode 100644 index 0000000000..ea742d0322 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// ObjCAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, + // this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific + // to the discarded scenes, as they will not return. + } + +} diff --git a/StoreKitTestApp/Assets.xcassets/AccentColor.colorset/Contents.json b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from StoreKitTestApp/Assets.xcassets/AccentColor.colorset/Contents.json rename to APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/StoreKitTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from StoreKitTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json rename to APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/StoreKitTestApp/Assets.xcassets/Contents.json b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/Contents.json similarity index 100% rename from StoreKitTestApp/Assets.xcassets/Contents.json rename to APITesters/ObjCAPITester/ObjCAPITesterIOS/Assets.xcassets/Contents.json diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/LaunchScreen.storyboard b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/Main.storyboard b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..25a763858e --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/Info.plist b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Info.plist new file mode 100644 index 0000000000..dd3c9afdae --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/SceneDelegate.swift b/APITesters/ObjCAPITester/ObjCAPITesterIOS/SceneDelegate.swift new file mode 100644 index 0000000000..53c87dcf52 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/SceneDelegate.swift @@ -0,0 +1,54 @@ +// +// SceneDelegate.swift +// ObjCAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` + // to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new + // (see `application:configurationForConnectingSceneSession` instead). + guard scene is UIWindowScene else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded + // (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + +} diff --git a/APITesters/ObjCAPITester/ObjCAPITesterIOS/ViewController.swift b/APITesters/ObjCAPITester/ObjCAPITesterIOS/ViewController.swift new file mode 100644 index 0000000000..ce8a2aec36 --- /dev/null +++ b/APITesters/ObjCAPITester/ObjCAPITesterIOS/ViewController.swift @@ -0,0 +1,17 @@ +// +// ViewController.swift +// ObjCAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + +} diff --git a/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj b/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj index ea30aed460..04b573ebf4 100644 --- a/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj +++ b/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj @@ -7,12 +7,33 @@ objects = { /* Begin PBXBuildFile section */ + 2D6F3873270E58DB002C9987 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 2D6F3872270E58DB002C9987 /* RevenueCat */; }; + 2D6F3874270E593F002C9987 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C626EBE7EA007DDB75 /* main.swift */; }; + 2DD778D3270E233F0079CBD4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778D2270E233F0079CBD4 /* AppDelegate.swift */; }; + 2DD778D5270E233F0079CBD4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778D4270E233F0079CBD4 /* SceneDelegate.swift */; }; + 2DD778D7270E233F0079CBD4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD778D6270E233F0079CBD4 /* ViewController.swift */; }; + 2DD778DA270E233F0079CBD4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DD778D8270E233F0079CBD4 /* Main.storyboard */; }; + 2DD778DC270E23400079CBD4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DD778DB270E23400079CBD4 /* Assets.xcassets */; }; + 2DD778DF270E23400079CBD4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DD778DD270E23400079CBD4 /* LaunchScreen.storyboard */; }; + 2DD778E4270E23460079CBD4 /* AttributionNetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614CA26EBE7EA007DDB75 /* AttributionNetworkAPI.swift */; }; + 2DD778E5270E23460079CBD4 /* IntroEligibilityAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614CD26EBE7EA007DDB75 /* IntroEligibilityAPI.swift */; }; + 2DD778E6270E23460079CBD4 /* PurchasesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614CE26EBE7EA007DDB75 /* PurchasesAPI.swift */; }; + 2DD778E7270E23460079CBD4 /* PurchasesErrorUtilsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614CC26EBE7EA007DDB75 /* PurchasesErrorUtilsAPI.swift */; }; + 2DD778E8270E23460079CBD4 /* PackageAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C926EBE7EA007DDB75 /* PackageAPI.swift */; }; + 2DD778E9270E23460079CBD4 /* CustomerInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C426EBE7EA007DDB75 /* CustomerInfoAPI.swift */; }; + 2DD778EA270E23460079CBD4 /* OfferingsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C326EBE7EA007DDB75 /* OfferingsAPI.swift */; }; + 2DD778EB270E23460079CBD4 /* ErrorCodesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C226EBE7EA007DDB75 /* ErrorCodesAPI.swift */; }; + 2DD778EC270E23460079CBD4 /* TransactionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614CB26EBE7EA007DDB75 /* TransactionAPI.swift */; }; + 2DD778ED270E23460079CBD4 /* OfferingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C726EBE7EA007DDB75 /* OfferingAPI.swift */; }; + 2DD778EE270E23460079CBD4 /* EntitlementInfosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C526EBE7EA007DDB75 /* EntitlementInfosAPI.swift */; }; + 2DD778EF270E23460079CBD4 /* EntitlementInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C826EBE7EA007DDB75 /* EntitlementInfoAPI.swift */; }; + A513AD34272B4C0100E0C1BA /* RefundRequestStatusAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A513AD33272B4C0100E0C1BA /* RefundRequestStatusAPI.swift */; }; + A513AD35272B4C0100E0C1BA /* RefundRequestStatusAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A513AD33272B4C0100E0C1BA /* RefundRequestStatusAPI.swift */; }; A55F62DB26EB001F00A1B466 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = A55F62DA26EB001F00A1B466 /* RevenueCat */; }; A5D614CF26EBE7EA007DDB75 /* ErrorCodesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C226EBE7EA007DDB75 /* ErrorCodesAPI.swift */; }; A5D614D026EBE7EA007DDB75 /* OfferingsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C326EBE7EA007DDB75 /* OfferingsAPI.swift */; }; A5D614D126EBE7EA007DDB75 /* CustomerInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C426EBE7EA007DDB75 /* CustomerInfoAPI.swift */; }; A5D614D226EBE7EA007DDB75 /* EntitlementInfosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C526EBE7EA007DDB75 /* EntitlementInfosAPI.swift */; }; - A5D614D326EBE7EA007DDB75 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C626EBE7EA007DDB75 /* main.swift */; }; A5D614D426EBE7EA007DDB75 /* OfferingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C726EBE7EA007DDB75 /* OfferingAPI.swift */; }; A5D614D526EBE7EA007DDB75 /* EntitlementInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C826EBE7EA007DDB75 /* EntitlementInfoAPI.swift */; }; A5D614D626EBE7EA007DDB75 /* PackageAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C926EBE7EA007DDB75 /* PackageAPI.swift */; }; @@ -35,6 +56,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2DD778D0270E233F0079CBD4 /* SwiftAPITesterIOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAPITesterIOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2DD778D2270E233F0079CBD4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2DD778D4270E233F0079CBD4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2DD778D6270E233F0079CBD4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 2DD778D9270E233F0079CBD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 2DD778DB270E23400079CBD4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2DD778DE270E23400079CBD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2DD778E0270E23400079CBD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A513AD33272B4C0100E0C1BA /* RefundRequestStatusAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundRequestStatusAPI.swift; sourceTree = ""; }; A55F62B526EAFFD200A1B466 /* SwiftAPITester */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SwiftAPITester; sourceTree = BUILT_PRODUCTS_DIR; }; A5D614C226EBE7EA007DDB75 /* ErrorCodesAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorCodesAPI.swift; sourceTree = ""; }; A5D614C326EBE7EA007DDB75 /* OfferingsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OfferingsAPI.swift; sourceTree = ""; }; @@ -51,6 +81,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2DD778CD270E233F0079CBD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D6F3873270E58DB002C9987 /* RevenueCat in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A55F62B226EAFFD200A1B466 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -62,11 +100,34 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2D6F3871270E58DB002C9987 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 2DD778D1270E233F0079CBD4 /* SwiftAPITesterIOS */ = { + isa = PBXGroup; + children = ( + 2DD778D2270E233F0079CBD4 /* AppDelegate.swift */, + 2DD778D4270E233F0079CBD4 /* SceneDelegate.swift */, + 2DD778D6270E233F0079CBD4 /* ViewController.swift */, + 2DD778D8270E233F0079CBD4 /* Main.storyboard */, + 2DD778DB270E23400079CBD4 /* Assets.xcassets */, + 2DD778DD270E23400079CBD4 /* LaunchScreen.storyboard */, + 2DD778E0270E23400079CBD4 /* Info.plist */, + ); + path = SwiftAPITesterIOS; + sourceTree = ""; + }; A55F62AC26EAFFD200A1B466 = { isa = PBXGroup; children = ( A55F62B726EAFFD200A1B466 /* SwiftAPITester */, + 2DD778D1270E233F0079CBD4 /* SwiftAPITesterIOS */, A55F62B626EAFFD200A1B466 /* Products */, + 2D6F3871270E58DB002C9987 /* Frameworks */, ); sourceTree = ""; }; @@ -74,6 +135,7 @@ isa = PBXGroup; children = ( A55F62B526EAFFD200A1B466 /* SwiftAPITester */, + 2DD778D0270E233F0079CBD4 /* SwiftAPITesterIOS.app */, ); name = Products; sourceTree = ""; @@ -93,6 +155,7 @@ A5D614C926EBE7EA007DDB75 /* PackageAPI.swift */, A5D614CE26EBE7EA007DDB75 /* PurchasesAPI.swift */, A5D614CB26EBE7EA007DDB75 /* TransactionAPI.swift */, + A513AD33272B4C0100E0C1BA /* RefundRequestStatusAPI.swift */, ); path = SwiftAPITester; sourceTree = ""; @@ -100,6 +163,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2DD778CF270E233F0079CBD4 /* SwiftAPITesterIOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2DD778E3270E23400079CBD4 /* Build configuration list for PBXNativeTarget "SwiftAPITesterIOS" */; + buildPhases = ( + 2DD778CC270E233F0079CBD4 /* Sources */, + 2DD778CD270E233F0079CBD4 /* Frameworks */, + 2DD778CE270E233F0079CBD4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftAPITesterIOS; + packageProductDependencies = ( + 2D6F3872270E58DB002C9987 /* RevenueCat */, + ); + productName = SwiftAPITesterIOS; + productReference = 2DD778D0270E233F0079CBD4 /* SwiftAPITesterIOS.app */; + productType = "com.apple.product-type.application"; + }; A55F62B426EAFFD200A1B466 /* SwiftAPITester */ = { isa = PBXNativeTarget; buildConfigurationList = A55F62BC26EAFFD200A1B466 /* Build configuration list for PBXNativeTarget "SwiftAPITester" */; @@ -130,6 +213,9 @@ LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1300; TargetAttributes = { + 2DD778CF270E233F0079CBD4 = { + CreatedOnToolsVersion = 13.0; + }; A55F62B426EAFFD200A1B466 = { CreatedOnToolsVersion = 13.0; }; @@ -152,17 +238,55 @@ projectRoot = ""; targets = ( A55F62B426EAFFD200A1B466 /* SwiftAPITester */, + 2DD778CF270E233F0079CBD4 /* SwiftAPITesterIOS */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 2DD778CE270E233F0079CBD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DD778DF270E23400079CBD4 /* LaunchScreen.storyboard in Resources */, + 2DD778DC270E23400079CBD4 /* Assets.xcassets in Resources */, + 2DD778DA270E233F0079CBD4 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ + 2DD778CC270E233F0079CBD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DD778EA270E23460079CBD4 /* OfferingsAPI.swift in Sources */, + 2DD778D7270E233F0079CBD4 /* ViewController.swift in Sources */, + A513AD35272B4C0100E0C1BA /* RefundRequestStatusAPI.swift in Sources */, + 2DD778E8270E23460079CBD4 /* PackageAPI.swift in Sources */, + 2DD778E6270E23460079CBD4 /* PurchasesAPI.swift in Sources */, + 2DD778D3270E233F0079CBD4 /* AppDelegate.swift in Sources */, + 2DD778EF270E23460079CBD4 /* EntitlementInfoAPI.swift in Sources */, + 2DD778E9270E23460079CBD4 /* CustomerInfoAPI.swift in Sources */, + 2DD778ED270E23460079CBD4 /* OfferingAPI.swift in Sources */, + 2DD778E7270E23460079CBD4 /* PurchasesErrorUtilsAPI.swift in Sources */, + 2DD778EE270E23460079CBD4 /* EntitlementInfosAPI.swift in Sources */, + 2DD778E5270E23460079CBD4 /* IntroEligibilityAPI.swift in Sources */, + 2DD778EB270E23460079CBD4 /* ErrorCodesAPI.swift in Sources */, + 2DD778D5270E233F0079CBD4 /* SceneDelegate.swift in Sources */, + 2DD778E4270E23460079CBD4 /* AttributionNetworkAPI.swift in Sources */, + 2DD778EC270E23460079CBD4 /* TransactionAPI.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A55F62B126EAFFD200A1B466 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A513AD34272B4C0100E0C1BA /* RefundRequestStatusAPI.swift in Sources */, A5D614CF26EBE7EA007DDB75 /* ErrorCodesAPI.swift in Sources */, - A5D614D326EBE7EA007DDB75 /* main.swift in Sources */, + 2D6F3874270E593F002C9987 /* main.swift in Sources */, A5D614D126EBE7EA007DDB75 /* CustomerInfoAPI.swift in Sources */, A5D614D026EBE7EA007DDB75 /* OfferingsAPI.swift in Sources */, A5D614D426EBE7EA007DDB75 /* OfferingAPI.swift in Sources */, @@ -178,7 +302,91 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 2DD778D8270E233F0079CBD4 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DD778D9270E233F0079CBD4 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 2DD778DD270E23400079CBD4 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DD778DE270E23400079CBD4 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + 2DD778E1270E23400079CBD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8SXR2327BM; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SwiftAPITesterIOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.SwiftAPITesterIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development com.revenuecat.SwiftAPITesterIOS"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2DD778E2270E23400079CBD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8SXR2327BM; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SwiftAPITesterIOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.SwiftAPITesterIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development com.revenuecat.SwiftAPITesterIOS"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; A55F62BA26EAFFD200A1B466 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -319,6 +527,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2DD778E3270E23400079CBD4 /* Build configuration list for PBXNativeTarget "SwiftAPITesterIOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2DD778E1270E23400079CBD4 /* Debug */, + 2DD778E2270E23400079CBD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; A55F62B026EAFFD200A1B466 /* Build configuration list for PBXProject "SwiftAPITester" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -351,6 +568,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 2D6F3872270E58DB002C9987 /* RevenueCat */ = { + isa = XCSwiftPackageProductDependency; + package = A55F62D926EB001F00A1B466 /* XCRemoteSwiftPackageReference "purchases-ios" */; + productName = RevenueCat; + }; A55F62DA26EB001F00A1B466 /* RevenueCat */ = { isa = XCSwiftPackageProductDependency; package = A55F62D926EB001F00A1B466 /* XCRemoteSwiftPackageReference "purchases-ios" */; diff --git a/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift b/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift index a7f3580e48..93ff962f4f 100644 --- a/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift +++ b/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift @@ -45,7 +45,10 @@ func checkPurchasesErrorCodeEnums() { .emptySubscriberAttributes, .productDiscountMissingIdentifierError, .missingAppUserIDForAliasCreationError, - .productDiscountMissingSubscriptionGroupIdentifierError: + .productDiscountMissingSubscriptionGroupIdentifierError, + .customerInfoError, + .systemInfoError, + .beginRefundRequestError: print(errCode!) } } diff --git a/APITesters/SwiftAPITester/SwiftAPITester/PackageAPI.swift b/APITesters/SwiftAPITester/SwiftAPITester/PackageAPI.swift index fb7864e865..5b2da49c78 100644 --- a/APITesters/SwiftAPITester/SwiftAPITester/PackageAPI.swift +++ b/APITesters/SwiftAPITester/SwiftAPITester/PackageAPI.swift @@ -19,7 +19,7 @@ var pack: Package! func checkPackageAPI() { let ident: String = pack.identifier let pType: PackageType = pack.packageType - let prod: SKProduct = pack.product + let prod: ProductDetails = pack.productDetails let lps: String = pack.localizedPriceString let lips: String = pack.localizedIntroductoryPriceString diff --git a/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 5cce6d8bce..338c3084e4 100644 --- a/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -118,6 +118,16 @@ private func checkPurchasesPurchasingAPI(purchases: Purchases) { purchases.purchase(package: pack, discount: skmd) { _, _, _, _ in } purchases.invalidateCustomerInfoCache() +#if os(iOS) || targetEnvironment(macCatalyst) + let beginRefundRequestCompletion: (RefundRequestStatus, Error?) -> Void = { _, _ in } + purchases.beginRefundRequest(for: "asdf", completion: beginRefundRequestCompletion) + purchases.beginRefundRequest(for: "asdf") { _, _ in } +#endif + +#if os(iOS) + purchases.presentCodeRedemptionSheet() +#endif + // PurchasesDelegate let customerInfo: CustomerInfo? = nil purchases.delegate?.purchases?(purchases, receivedUpdated: customerInfo!) diff --git a/APITesters/SwiftAPITester/SwiftAPITester/RefundRequestStatusAPI.swift b/APITesters/SwiftAPITester/SwiftAPITester/RefundRequestStatusAPI.swift new file mode 100644 index 0000000000..f736ec5a69 --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITester/RefundRequestStatusAPI.swift @@ -0,0 +1,19 @@ +// +// RefundRequestStatusAPI.swift +// SwiftAPITester +// +// Created by Madeline Beyl on 10/28/21. +// + +import Foundation +import RevenueCat + +var refundStatus: RefundRequestStatus! +func checkRefundRequestStatusEnum() { + switch refundStatus! { + case .userCancelled, + .success, + .error: + print(refundStatus!) + } +} diff --git a/APITesters/SwiftAPITester/SwiftAPITester/main.swift b/APITesters/SwiftAPITester/SwiftAPITester/main.swift index bd9c242fc6..3c3ab1dec5 100644 --- a/APITesters/SwiftAPITester/SwiftAPITester/main.swift +++ b/APITesters/SwiftAPITester/SwiftAPITester/main.swift @@ -38,4 +38,6 @@ checkPurchasesErrorCodeEnums() checkPackageAPI() checkPackageEnums() +checkRefundRequestStatusEnum() + checkTransactionAPI() diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/AppDelegate.swift b/APITesters/SwiftAPITester/SwiftAPITesterIOS/AppDelegate.swift new file mode 100644 index 0000000000..134496e8d3 --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// SwiftAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, + // this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + +} diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AccentColor.colorset/Contents.json b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StoreKitTestApp/Preview Content/Preview Assets.xcassets/Contents.json b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/Contents.json similarity index 100% rename from StoreKitTestApp/Preview Content/Preview Assets.xcassets/Contents.json rename to APITesters/SwiftAPITester/SwiftAPITesterIOS/Assets.xcassets/Contents.json diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/LaunchScreen.storyboard b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/Main.storyboard b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..25a763858e --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/Info.plist b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Info.plist new file mode 100644 index 0000000000..dd3c9afdae --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/SceneDelegate.swift b/APITesters/SwiftAPITester/SwiftAPITesterIOS/SceneDelegate.swift new file mode 100644 index 0000000000..0f938236fc --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/SceneDelegate.swift @@ -0,0 +1,54 @@ +// +// SceneDelegate.swift +// SwiftAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to + // the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new + // (see `application:configurationForConnectingSceneSession` instead). + guard scene is UIWindowScene else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded + // (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + +} diff --git a/APITesters/SwiftAPITester/SwiftAPITesterIOS/ViewController.swift b/APITesters/SwiftAPITester/SwiftAPITesterIOS/ViewController.swift new file mode 100644 index 0000000000..cd6cc516cb --- /dev/null +++ b/APITesters/SwiftAPITester/SwiftAPITesterIOS/ViewController.swift @@ -0,0 +1,17 @@ +// +// ViewController.swift +// SwiftAPITesterIOS +// +// Created by Andrés Boedo on 10/6/21. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + +} diff --git a/BackendIntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json b/BackendIntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/BackendIntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BackendIntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/BackendIntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/BackendIntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BackendIntegrationTestApp/Assets.xcassets/Contents.json b/BackendIntegrationTestApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/BackendIntegrationTestApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StoreKitTestApp/StoreKitTestAppApp.swift b/BackendIntegrationTestApp/BackendIntegrationTestApp.swift similarity index 67% rename from StoreKitTestApp/StoreKitTestAppApp.swift rename to BackendIntegrationTestApp/BackendIntegrationTestApp.swift index cfd3041e4f..ea7a342ea9 100644 --- a/StoreKitTestApp/StoreKitTestAppApp.swift +++ b/BackendIntegrationTestApp/BackendIntegrationTestApp.swift @@ -1,6 +1,6 @@ // -// StoreKitTestAppApp.swift -// StoreKitTestApp +// BackendIntegrationTestApp.swift +// BackendIntegrationTestApp // // Created by Andrés Boedo on 5/3/21. // Copyright © 2021 Purchases. All rights reserved. @@ -9,7 +9,7 @@ import SwiftUI @main -struct StoreKitTestAppApp: App { +struct BackendIntegrationTestApp: App { var body: some Scene { WindowGroup { ContentView() diff --git a/StoreKitTestApp/ContentView.swift b/BackendIntegrationTestApp/ContentView.swift similarity index 92% rename from StoreKitTestApp/ContentView.swift rename to BackendIntegrationTestApp/ContentView.swift index 9ddd1051a4..ba963aaf27 100644 --- a/StoreKitTestApp/ContentView.swift +++ b/BackendIntegrationTestApp/ContentView.swift @@ -1,6 +1,6 @@ // // ContentView.swift -// StoreKitTestApp +// BackendIntegrationTestApp // // Created by Andrés Boedo on 5/3/21. // Copyright © 2021 Purchases. All rights reserved. diff --git a/StoreKitTestApp/Info.plist b/BackendIntegrationTestApp/Info.plist similarity index 95% rename from StoreKitTestApp/Info.plist rename to BackendIntegrationTestApp/Info.plist index 0ae2e5aaa3..66fac79d30 100644 --- a/StoreKitTestApp/Info.plist +++ b/BackendIntegrationTestApp/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + BackendIntegrationTestApp CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/BackendIntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json b/BackendIntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/BackendIntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StoreKitTests/StoreKitTests.swift b/BackendIntegrationTests/BackendIntegrationTests.swift similarity index 69% rename from StoreKitTests/StoreKitTests.swift rename to BackendIntegrationTests/BackendIntegrationTests.swift index a8806eb0a4..7d28ba6de1 100644 --- a/StoreKitTests/StoreKitTests.swift +++ b/BackendIntegrationTests/BackendIntegrationTests.swift @@ -1,6 +1,6 @@ // -// StoreKitTests.swift -// StoreKitTests +// BackendIntegrationTests.swift +// BackendIntegrationTests // // Created by Andrés Boedo on 5/3/21. // Copyright © 2021 Purchases. All rights reserved. @@ -12,21 +12,21 @@ import Nimble import StoreKitTest class TestPurchaseDelegate: NSObject, PurchasesDelegate { - var purchaserInfo: CustomerInfo? + var maybeCustomerInfo: CustomerInfo? var purchaserInfoUpdateCount = 0 - func purchases(_ purchases: Purchases, receivedUpdated purchaserInfo: CustomerInfo) { - self.purchaserInfo = purchaserInfo + func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) { + self.maybeCustomerInfo = customerInfo purchaserInfoUpdateCount += 1 } func purchases(_ purchases: Purchases, - shouldPurchasePromoProduct product: SKProduct, + shouldPurchasePromoProduct product: SK1Product, defermentBlock makeDeferredPurchase: @escaping DeferredPromotionalPurchaseBlock) { } } -class StoreKitTests: XCTestCase { +class BackendIntegrationTests: XCTestCase { var testSession: SKTestSession! var userDefaults: UserDefaults! @@ -66,7 +66,7 @@ class StoreKitTests: XCTestCase { purchaseMonthlyOffering() waitUntilEntitlementsGoThrough() - let entitlements = purchasesDelegate.purchaserInfo?.entitlements + let entitlements = purchasesDelegate.maybeCustomerInfo?.entitlements expect(entitlements?["premium"]?.isActive) == true } @@ -76,7 +76,7 @@ class StoreKitTests: XCTestCase { var completionCalled = false purchaseMonthlyOffering { [self] purchaserInfo, error in expect(purchaserInfo?.entitlements.all.count) == 1 - let entitlements = self.purchasesDelegate.purchaserInfo?.entitlements + let entitlements = self.purchasesDelegate.maybeCustomerInfo?.entitlements expect(entitlements?["premium"]?.isActive) == true let anonUserID = Purchases.shared.appUserID @@ -199,9 +199,9 @@ class StoreKitTests: XCTestCase { expect(error).to(beNil()) } - expect(self.purchasesDelegate.purchaserInfo?.originalAppUserId) + expect(self.purchasesDelegate.maybeCustomerInfo?.originalAppUserId) .toEventually(equal(userID2), timeout: .seconds(10)) - assertNoPurchases(purchasesDelegate.purchaserInfo) + assertNoPurchases(purchasesDelegate.maybeCustomerInfo) } func testLogOutRemovesEntitlements() { @@ -230,9 +230,76 @@ class StoreKitTests: XCTestCase { expect(completionCalled).toEventually(beTrue(), timeout: .seconds(10)) } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func testEligibleForIntroBeforePurchaseAndIneligibleAfter() throws { + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + configurePurchases() + + var maybeProductID: String? + var completionCalled = false + var maybeReceivedEligibility: [String: IntroEligibility]? + + Purchases.shared.getOfferings { offerings, error in + maybeProductID = offerings?.current?.monthly?.productDetails.productIdentifier + completionCalled = true + } + + expect(completionCalled).toEventually(beTrue(), timeout: .seconds(10)) + completionCalled = false + + let productID = try XCTUnwrap(maybeProductID) + + Purchases.shared.checkTrialOrIntroductoryPriceEligibility([productID]) { receivedEligibility in + completionCalled = true + maybeReceivedEligibility = receivedEligibility + } + + expect(completionCalled).toEventually(beTrue(), timeout: .seconds(10)) + completionCalled = false + + var receivedEligibility = try XCTUnwrap(maybeReceivedEligibility) + expect(receivedEligibility[productID]?.status) == .eligible + + purchaseMonthlyOffering { [self] purchaserInfo, error in + expect(purchaserInfo?.entitlements.all.count) == 1 + let entitlements = self.purchasesDelegate.maybeCustomerInfo?.entitlements + expect(entitlements?["premium"]?.isActive) == true + + let anonUserID = Purchases.shared.appUserID + let identifiedUserID = "\(#function)_\(anonUserID)_".replacingOccurrences(of: "RCAnonymous", with: "") + + Purchases.shared.logIn(identifiedUserID) { identifiedPurchaserInfo, created, error in + expect(error).to(beNil()) + + expect(created).to(beTrue()) + expect(identifiedPurchaserInfo?.entitlements["premium"]?.isActive) == true + completionCalled = true + } + } + + expect(completionCalled).toEventually(beTrue(), timeout: .seconds(10)) + completionCalled = false + + Purchases.shared.checkTrialOrIntroductoryPriceEligibility([productID]) { receivedEligibility in + completionCalled = true + maybeReceivedEligibility = receivedEligibility + } + + expect(completionCalled).toEventually(beTrue(), timeout: .seconds(10)) + + receivedEligibility = try XCTUnwrap(maybeReceivedEligibility) + expect(receivedEligibility[productID]?.status) == .ineligible + } + } -private extension StoreKitTests { +private extension BackendIntegrationTests { func purchaseMonthlyOffering(completion: ((CustomerInfo?, Error?) -> Void)? = nil) { Purchases.shared.getOfferings { offerings, error in @@ -244,10 +311,10 @@ private extension StoreKitTests { let monthlyPackage = offering?.monthly expect(monthlyPackage).toNot(beNil()) - Purchases.shared.purchase(product: monthlyPackage!.product) { transaction, - purchaserInfo, - purchaseError, - userCancelled in + Purchases.shared.purchase(package: monthlyPackage!) { transaction, + purchaserInfo, + purchaseError, + userCancelled in expect(purchaseError).to(beNil()) expect(purchaserInfo).toNot(beNil()) } @@ -269,7 +336,7 @@ private extension StoreKitTests { } func waitUntilEntitlementsGoThrough() { - expect(self.purchasesDelegate.purchaserInfo?.entitlements.all.count) + expect(self.purchasesDelegate.maybeCustomerInfo?.entitlements.all.count) .toEventually(equal(1), timeout: .seconds(10)) } diff --git a/StoreKitTests/Constants.swift b/BackendIntegrationTests/Constants.swift similarity index 81% rename from StoreKitTests/Constants.swift rename to BackendIntegrationTests/Constants.swift index 980c922a9a..4eb5f606bd 100644 --- a/StoreKitTests/Constants.swift +++ b/BackendIntegrationTests/Constants.swift @@ -1,6 +1,6 @@ // // Constants.swift -// StoreKitTests +// BackendIntegrationTests // // Created by Andrés Boedo on 5/4/21. // Copyright © 2021 Purchases. All rights reserved. @@ -13,6 +13,6 @@ struct Constants { // Server URL for the tests. If set to empty string, we'll use the default URL. static let proxyURL = "REVENUECAT_PROXY_URL" - static let userDefaultsSuiteName = "StoreKitTests" + static let userDefaultsSuiteName = "BackendIntegrationTests" static let storeKitConfigFileName = "Configuration" } diff --git a/StoreKitTests/Info.plist b/BackendIntegrationTests/Info.plist similarity index 100% rename from StoreKitTests/Info.plist rename to BackendIntegrationTests/Info.plist diff --git a/Examples/MagicWeatherSwiftUI/Magic Weather SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/MagicWeatherSwiftUI/Magic Weather SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..e69de29bb2 diff --git a/IntegrationTests/PurchaseTester/Configuration.storekit b/IntegrationTests/PurchaseTester/Configuration.storekit index bf6bbecd61..fdf36c4215 100644 --- a/IntegrationTests/PurchaseTester/Configuration.storekit +++ b/IntegrationTests/PurchaseTester/Configuration.storekit @@ -46,8 +46,8 @@ }, "localizations" : [ { - "description" : "", - "displayName" : "", + "description" : "Monthly subscription with a 1-week free trial", + "displayName" : "Monthly Free Trial", "locale" : "en_US" } ], diff --git a/IntegrationTests/PurchaseTester/PurchaseTester/SwiftPaywall.swift b/IntegrationTests/PurchaseTester/PurchaseTester/SwiftPaywall.swift index 02a33fe8f7..46cc181c55 100644 --- a/IntegrationTests/PurchaseTester/PurchaseTester/SwiftPaywall.swift +++ b/IntegrationTests/PurchaseTester/PurchaseTester/SwiftPaywall.swift @@ -272,8 +272,8 @@ class SwiftPaywall: UIViewController { private func shouldShowDiscount(package: Package?) -> (Bool, Package?) { return (showDiscountPercentage == true - && mostAffordablePackages.count > 1 - && mostAffordablePackages.first?.product.productIdentifier == package?.product.productIdentifier, mostAffordablePackages.last) + && mostAffordablePackages.count > 1 + && mostAffordablePackages.first?.productDetails.productIdentifier == package?.productDetails.productIdentifier, mostAffordablePackages.last) } private var mostAffordablePackages : [Package] { @@ -530,7 +530,7 @@ extension SwiftPaywall: UICollectionViewDelegate, UICollectionViewDataSource, UI productDeselectedColor: productDeselectedColor) // Should this package be selected - if !didChangePackage && mostAffordablePackages.first?.product.productIdentifier == package?.product.productIdentifier { + if !didChangePackage && mostAffordablePackages.first?.productDetails.productIdentifier == package?.productDetails.productIdentifier { collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath) cell.isSelected = true @@ -542,15 +542,18 @@ extension SwiftPaywall: UICollectionViewDelegate, UICollectionViewDataSource, UI func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { didChangePackage = true - + if #available(iOS 11.2, *) { - if let introPrice = offering?.availablePackages[indexPath.row].product.introductoryPrice, introPrice.price == 0 { + // todo: remove this check when sk2 products support introductory price + // https://github.com/RevenueCat/purchases-ios/issues/848 + if let sk1ProductDetails = offering?.availablePackages[indexPath.row].productDetails as? SK1ProductDetails, + let introPrice = sk1ProductDetails.underlyingSK1Product.introductoryPrice, introPrice.price == 0 { var trialLength = "" var cancelDate : Date? var cancelString = "end of trial" let numUnits = introPrice.subscriptionPeriod.numberOfUnits - + switch introPrice.subscriptionPeriod.unit { case .day: trialLength = "\(numUnits)-day" @@ -569,18 +572,18 @@ extension SwiftPaywall: UICollectionViewDelegate, UICollectionViewDataSource, UI @unknown default: fatalError() } - + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMMM d" if let cancelDate = cancelDate { cancelString = dateFormatter.string(from: cancelDate) } - + let dateAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14)] let baseText = NSMutableAttributedString(string: "Includes \(trialLength) free trial. Cancel before ") let cancelAttributedText = NSAttributedString(string: cancelString, attributes: dateAttributes) let and = NSAttributedString(string: " and nothing will be billed.") - + baseText.append(cancelAttributedText) baseText.append(and) @@ -674,7 +677,7 @@ private class PackageCell : UICollectionViewCell { }() let discountFormatter = NumberFormatter() - let priceFormatter = NumberFormatter() + var maybePriceFormatter: NumberFormatter? = nil var highlightColor : UIColor? var secondaryColor : UIColor? @@ -703,6 +706,14 @@ private class PackageCell : UICollectionViewCell { } } + fileprivate func setMonthlyPriceLabel(_ package: Package, numberOfMonths: Int) { + if let priceFormatter = maybePriceFormatter, + let sk1ProductDetails = package.productDetails as? SK1ProductDetails { + let monthlyPrice = sk1ProductDetails.underlyingSK1Product.price.dividing(by: Decimal(numberOfMonths) as NSDecimalNumber) + monthlyPriceLabel.text = "\(priceFormatter.string(from: monthlyPrice) ?? "") / mo" + } + } + func setupWith( package: Package?, discount: (Bool, Package?), @@ -741,8 +752,12 @@ private class PackageCell : UICollectionViewCell { discountLabel.text = "SAVE \(discountFormatter.string(from: discountBetween(highest: discount, current: package)) ?? "")" } - priceFormatter.numberStyle = .currency - priceFormatter.locale = package.product.priceLocale + if let sk1ProductDetails = package.productDetails as? SK1ProductDetails { + maybePriceFormatter = NumberFormatter() + maybePriceFormatter?.numberStyle = .currency + maybePriceFormatter?.locale = sk1ProductDetails.underlyingSK1Product.priceLocale + } + priceLabel.text = package.localizedPriceString @@ -753,16 +768,16 @@ private class PackageCell : UICollectionViewCell { discountLabel.isHidden = true case .annual: durationLabel.text = "1\nYEAR" - monthlyPriceLabel.text = "\(priceFormatter.string(from: package.product.price.dividing(by: 12.0)) ?? "") / mo" + setMonthlyPriceLabel(package, numberOfMonths: 12) case .sixMonth: durationLabel.text = "6\nMONTHS" - monthlyPriceLabel.text = "\(priceFormatter.string(from: package.product.price.dividing(by: 6.0)) ?? "") / mo" + setMonthlyPriceLabel(package, numberOfMonths: 6) case .threeMonth: durationLabel.text = "3\nMONTHS" - monthlyPriceLabel.text = "\(priceFormatter.string(from: package.product.price.dividing(by: 3.0)) ?? "") / mo" + setMonthlyPriceLabel(package, numberOfMonths: 3) case .twoMonth: durationLabel.text = "2\nMONTHS" - monthlyPriceLabel.text = "\(priceFormatter.string(from: package.product.price.dividing(by: 2.0)) ?? "") / mo" + setMonthlyPriceLabel(package, numberOfMonths: 2) case .monthly: durationLabel.text = "1\nMONTH" monthlyPriceLabel.text = "\(package.localizedPriceString) / mo" @@ -777,43 +792,43 @@ private class PackageCell : UICollectionViewCell { } func discountBetween(highest: Package, current: Package) -> NSNumber { - let highestAnnualCost : NSNumber! + let highestAnnualCost : Decimal! switch highest.packageType { case .annual: - highestAnnualCost = highest.product.price + highestAnnualCost = highest.productDetails.price case .sixMonth: - highestAnnualCost = highest.product.price.multiplying(by: 2.0) + highestAnnualCost = highest.productDetails.price * 2.0 case .threeMonth: - highestAnnualCost = highest.product.price.multiplying(by: 4.0) + highestAnnualCost = highest.productDetails.price * 4.0 case .twoMonth: - highestAnnualCost = highest.product.price.multiplying(by: 6.0) + highestAnnualCost = highest.productDetails.price * 6.0 case .monthly: - highestAnnualCost = highest.product.price.multiplying(by: 12.0) + highestAnnualCost = highest.productDetails.price * 12.0 case .weekly: - highestAnnualCost = highest.product.price.multiplying(by: 52.0) + highestAnnualCost = highest.productDetails.price * 52.0 case .lifetime, .custom, .unknown: return 0.0 } - let currentAnnualCost : NSNumber! + let currentAnnualCost : Decimal! switch current.packageType { case .annual: - currentAnnualCost = current.product.price + currentAnnualCost = current.productDetails.price case .sixMonth: - currentAnnualCost = current.product.price.multiplying(by: 2.0) + currentAnnualCost = current.productDetails.price * 2.0 case .threeMonth: - currentAnnualCost = current.product.price.multiplying(by: 4.0) + currentAnnualCost = current.productDetails.price * 4.0 case .twoMonth: - currentAnnualCost = current.product.price.multiplying(by: 6.0) + currentAnnualCost = current.productDetails.price * 6.0 case .monthly: - currentAnnualCost = current.product.price.multiplying(by: 12.0) + currentAnnualCost = current.productDetails.price * 12.0 case .weekly: - currentAnnualCost = current.product.price.multiplying(by: 52.0) + currentAnnualCost = current.productDetails.price * 52.0 case .lifetime, .custom, .unknown: return 0.0 } - return NSNumber(value: (highestAnnualCost.doubleValue - currentAnnualCost.doubleValue) / highestAnnualCost.doubleValue) + return NSNumber(nonretainedObject: (highestAnnualCost - currentAnnualCost) / highestAnnualCost) } func buildSubviews() { @@ -867,20 +882,20 @@ private class PackageCell : UICollectionViewCell { fileprivate extension Package { - func annualCost() -> Double { + func annualCost() -> Decimal { switch self.packageType { case .annual: - return self.product.price.doubleValue + return self.productDetails.price case .sixMonth: - return self.product.price.doubleValue * 2 + return self.productDetails.price * 2 case .threeMonth: - return self.product.price.doubleValue * 4 + return self.productDetails.price * 4 case .twoMonth: - return self.product.price.doubleValue * 6 + return self.productDetails.price * 6 case .monthly: - return self.product.price.doubleValue * 12 + return self.productDetails.price * 12 case .weekly: - return self.product.price.doubleValue * 52 + return self.productDetails.price * 52 case .lifetime, .custom, .unknown: return 0.0 } diff --git a/IntegrationTests/SPMIntegration/fastlane/README.md b/IntegrationTests/SPMIntegration/fastlane/README.md index 9c29cab552..206e702a74 100644 --- a/IntegrationTests/SPMIntegration/fastlane/README.md +++ b/IntegrationTests/SPMIntegration/fastlane/README.md @@ -101,9 +101,9 @@ fastlane ios export_xcframework Export XCFramework ### ios storekit_tests ``` -fastlane ios storekit_tests +fastlane ios backend_integration_tests ``` -Run StoreKitTests +Run BackendIntegrationTests ### ios update_swift_package_commit ``` fastlane ios update_swift_package_commit diff --git a/IntegrationTests/XcodeDirectIntegration/XcodeDirectIntegration.xcodeproj/project.pbxproj b/IntegrationTests/XcodeDirectIntegration/XcodeDirectIntegration.xcodeproj/project.pbxproj index 0f1e6fd9bf..e9815e10a3 100644 --- a/IntegrationTests/XcodeDirectIntegration/XcodeDirectIntegration.xcodeproj/project.pbxproj +++ b/IntegrationTests/XcodeDirectIntegration/XcodeDirectIntegration.xcodeproj/project.pbxproj @@ -56,7 +56,7 @@ containerPortal = 2D6C0454254A0AF10039CB92 /* RevenueCat.xcodeproj */; proxyType = 2; remoteGlobalIDString = 2DE20B6C264087FB004C597D; - remoteInfo = StoreKitTests; + remoteInfo = BackendIntegrationTests; }; /* End PBXContainerItemProxy section */ @@ -184,7 +184,8 @@ B3EAF56B26E29DCC00E0728D /* RevenueCat.framework */, B3EAF56D26E29DCC00E0728D /* RevenueCatTests.xctest */, B3EAF56F26E29DCC00E0728D /* StoreKitTestApp.app */, - B3EAF57126E29DCC00E0728D /* StoreKitTests.xctest */, + B3EAF57126E29DCC00E0728D /* BackendIntegrationTests.xctest */, + B3EAF57326E29DCC00E0728D /* APITester */, ); name = Products; sourceTree = ""; @@ -313,10 +314,10 @@ remoteRef = B3EAF56E26E29DCC00E0728D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - B3EAF57126E29DCC00E0728D /* StoreKitTests.xctest */ = { + B3EAF57126E29DCC00E0728D /* BackendIntegrationTests.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = StoreKitTests.xctest; + path = BackendIntegrationTests.xctest; remoteRef = B3EAF57026E29DCC00E0728D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; diff --git a/Package.swift b/Package.swift index 0fd2e4dfc9..8b3cd4b494 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ func resolveTargets() -> [Target] { let baseTargets: [Target] = [ .target(name: "RevenueCat", path: ".", - exclude: ["Purchases/Info.plist", "RequiresXcode13"], + exclude: ["Purchases/Info.plist"], sources: ["Purchases"] ) // Note: publicHeadersPath: "Purchases/Public" doesn't actually pick up .h headers in Swift projects. @@ -20,7 +20,15 @@ func resolveTargets() -> [Target] { let package = Package( name: "RevenueCat", platforms: [ - .macOS(.v10_12), .iOS(.v9), .watchOS("6.2"), .tvOS(.v9) + .macOS(.v10_12), + .watchOS("6.2"), + .tvOS(.v9), + + // todo: deployment_target set to 12.0 instead of 9.0 temporarily for iOS due to a known issue in + // Xcode-beta 5, where swift libraries fail to build for iOS targets that use armv7. + // See issue 74120874 in the release notes: + // https://developer.apple.com/documentation/xcode-release-notes/xcode-13-beta-release-notes + .iOS(.v12) ], products: [ .library(name: "RevenueCat", diff --git a/Purchases/Error Handling/ErrorUtils.swift b/Purchases/Error Handling/ErrorUtils.swift index 1fe748f71f..0bab94e9d8 100644 --- a/Purchases/Error Handling/ErrorUtils.swift +++ b/Purchases/Error Handling/ErrorUtils.swift @@ -31,7 +31,7 @@ class ErrorUtils: NSObject { } /** - * Maps an ``BackendErrorCode`` code to a ``ErrorCode``. code. Constructs an Error with the mapped code and adds a + * Maps a ``BackendErrorCode`` code to a ``ErrorCode``. code. Constructs an Error with the mapped code and adds a * `NSUnderlyingErrorKey` in the `NSError.userInfo` dictionary. The backend error code will be mapped using * ``BackendErrorCode/toPurchasesErrorCode()``. * @@ -46,7 +46,7 @@ class ErrorUtils: NSObject { } /** - * Maps an ``BackendErrorCode`` code to an ``ErrorCode``. code. Constructs an Error with the mapped code and adds a + * Maps a ``BackendErrorCode`` code to an ``ErrorCode``. code. Constructs an Error with the mapped code and adds a * `RCUnderlyingErrorKey` in the `NSError.userInfo` dictionary. The backend error code will be mapped using * ``BackendErrorCode/toPurchasesErrorCode()``. * @@ -207,55 +207,64 @@ class ErrorUtils: NSObject { return error(with: errorCode, message: errorCode.description, underlyingError: skError) } -} + /** + * Constructs an Error with the ``ErrorCode/purchaseCancelledError`` code. + * + * - Note: This error is used when a purchase is cancelled by the user. + */ + @objc static func purchaseCancelledError() -> Error { + let errorCode = ErrorCode.purchaseCancelledError + return ErrorUtils.error(with: errorCode, + message: errorCode.description, + underlyingError: nil) + } -private extension SKError { + /** + * Constructs an Error with the ``ErrorCode/storeProblemError`` code. + * + * - Note: This error is used when there is a problem with the App Store. + */ + @objc static func storeProblemError(withMessage message: String, error: Error? = nil) -> Error { + let errorCode = ErrorCode.storeProblemError + return ErrorUtils.error(with: errorCode, + message: message, + underlyingError: error) + } - // swiftlint:disable:next cyclomatic_complexity - func toPurchasesErrorCode() -> ErrorCode { - switch self.code { - case .cloudServiceNetworkConnectionFailed, - .cloudServiceRevoked, - .overlayTimeout, - .overlayPresentedInBackgroundScene: - return .storeProblemError - case .clientInvalid, - .paymentNotAllowed, - .cloudServicePermissionDenied, - .privacyAcknowledgementRequired: - return .purchaseNotAllowedError - case .paymentCancelled, - .overlayCancelled: - return .purchaseCancelledError - case .paymentInvalid, - .unauthorizedRequestData, - .missingOfferParams, - .invalidOfferPrice, - .invalidSignature, - .invalidOfferIdentifier: - return .purchaseInvalidError - case .storeProductNotAvailable: - return .productNotAvailableForPurchaseError - case .ineligibleForOffer, - .overlayInvalidConfiguration, - .unsupportedPlatform: - return .purchaseNotAllowedError - case .unknown: - if let error = self.userInfo[NSUnderlyingErrorKey] as? NSError { - switch (error.domain, error.code) { - case ("ASDServerErrorDomain", 3532): // "You’re currently subscribed to this" - // See https://github.com/RevenueCat/purchases-ios/issues/392 - return .productAlreadyPurchasedError + /** + * Constructs an Error with the ``ErrorCode/customerInfoError`` code. + * + * - Note: This error is used when there is a problem related to the customer info. + */ + @objc static func customerInfoError(withMessage message: String, error: Error? = nil) -> Error { + let errorCode = ErrorCode.customerInfoError + return ErrorUtils.error(with: errorCode, + message: message, + underlyingError: error) + } - default: break - } - } + /** + * Constructs an Error with the ``ErrorCode/systemInfoError`` code. + * + * - Note: This error is used when there is a problem related to the system info. + */ + @objc static func systemInfoError(withMessage message: String, error: Error? = nil) -> Error { + let errorCode = ErrorCode.systemInfoError + return ErrorUtils.error(with: errorCode, + message: message, + underlyingError: error) + } - return .storeProblemError - - @unknown default: - return .unknownError - } + /** + * Constructs an Error with the ``ErrorCode/beginRefundRequestError`` code. + * + * - Note: This error is used when there is a problem beginning a refund request. + */ + @objc static func beginRefundRequestError(withMessage message: String, error: Error? = nil) -> Error { + let errorCode = ErrorCode.beginRefundRequestError + return ErrorUtils.error(with: errorCode, + message: message, + underlyingError: error) } } @@ -351,7 +360,10 @@ private extension ErrorUtils { .emptySubscriberAttributes, .productDiscountMissingIdentifierError, .missingAppUserIDForAliasCreationError, - .productDiscountMissingSubscriptionGroupIdentifierError: + .productDiscountMissingSubscriptionGroupIdentifierError, + .customerInfoError, + .systemInfoError, + .beginRefundRequestError: Logger.error(code.description) case .purchaseCancelledError, diff --git a/Purchases/Error Handling/SKError+Extensions.swift b/Purchases/Error Handling/SKError+Extensions.swift new file mode 100644 index 0000000000..c77e98fb49 --- /dev/null +++ b/Purchases/Error Handling/SKError+Extensions.swift @@ -0,0 +1,66 @@ +// +// 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 +// +// SKError+Extensions.swift +// +// Created by Madeline Beyl on 11/4/21. + +import Foundation +import StoreKit + +extension SKError { + + // swiftlint:disable:next cyclomatic_complexity + func toPurchasesErrorCode() -> ErrorCode { + switch self.code { + case .cloudServiceNetworkConnectionFailed, + .cloudServiceRevoked, + .overlayTimeout, + .overlayPresentedInBackgroundScene: + return .storeProblemError + case .clientInvalid, + .paymentNotAllowed, + .cloudServicePermissionDenied, + .privacyAcknowledgementRequired: + return .purchaseNotAllowedError + case .paymentCancelled, + .overlayCancelled: + return .purchaseCancelledError + case .paymentInvalid, + .unauthorizedRequestData, + .missingOfferParams, + .invalidOfferPrice, + .invalidSignature, + .invalidOfferIdentifier: + return .purchaseInvalidError + case .storeProductNotAvailable: + return .productNotAvailableForPurchaseError + case .ineligibleForOffer, + .overlayInvalidConfiguration, + .unsupportedPlatform: + return .purchaseNotAllowedError + case .unknown: + if let error = self.userInfo[NSUnderlyingErrorKey] as? NSError { + switch (error.domain, error.code) { + case ("ASDServerErrorDomain", 3532): // "You’re currently subscribed to this" + // See https://github.com/RevenueCat/purchases-ios/issues/392 + return .productAlreadyPurchasedError + + default: break + } + } + + return .storeProblemError + + @unknown default: + return .unknownError + } + } + +} diff --git a/Purchases/FoundationExtensions/Dictionary+Extensions.swift b/Purchases/FoundationExtensions/Dictionary+Extensions.swift index 0ae881c3f0..92a845aa67 100644 --- a/Purchases/FoundationExtensions/Dictionary+Extensions.swift +++ b/Purchases/FoundationExtensions/Dictionary+Extensions.swift @@ -75,4 +75,16 @@ extension Dictionary { lhs.merging(rhs) } + /// Adds values from rhs to lhs dictionary + /// + /// The merge strategy used is `overwriteValue`. + /// + /// - Parameters: + /// - lhs: A dictionary to merge. + /// - rhs: Another dictionary to merge. + /// - Returns: An dictionary with keys and values from both. + static func += (lhs: inout [Key: Value], rhs: [Key: Value]) { + lhs = lhs.merging(rhs) + } + } diff --git a/Purchases/Logging/Strings/CustomerInfoStrings.swift b/Purchases/Logging/Strings/CustomerInfoStrings.swift index a47cce3093..0b9f620d45 100644 --- a/Purchases/Logging/Strings/CustomerInfoStrings.swift +++ b/Purchases/Logging/Strings/CustomerInfoStrings.swift @@ -18,7 +18,7 @@ import Foundation enum CustomerInfoStrings { case checking_intro_eligibility_locally_error(error: Error) - case checking_intro_eligibility_locally_result(productIdentifiers: [String: NSNumber]) + case checking_intro_eligibility_locally_result(productIdentifiers: [String: IntroEligibilityStatus]) case checking_intro_eligibility_locally case invalidating_purchaserinfo_cache case no_cached_purchaserinfo diff --git a/Purchases/Logging/Strings/PurchaseStrings.swift b/Purchases/Logging/Strings/PurchaseStrings.swift index 6a905820d9..b27130a626 100644 --- a/Purchases/Logging/Strings/PurchaseStrings.swift +++ b/Purchases/Logging/Strings/PurchaseStrings.swift @@ -38,6 +38,17 @@ enum PurchaseStrings { case purchases_delegate_set_multiple_times case purchases_delegate_set_to_nil case management_url_nil_opening_default + case requested_products_not_found(request: SKRequest) + case callback_not_found_for_request(request: SKRequest) + case unable_to_get_intro_eligibility_for_user(error: Error) + case duplicate_refund_request(details: String) + case failed_refund_request(details: String) + case unknown_refund_request_error(details: String) + case unknown_refund_request_error_type(details: String) + case unknown_refund_request_status + case product_unpurchased_or_missing + case transaction_unverified(productID: String, errorMessage: String) + case begin_refund_request_unsupported } @@ -48,42 +59,42 @@ extension PurchaseStrings: CustomStringConvertible { case .cannot_purchase_product_appstore_configuration_error: return "Could not purchase SKProduct. " + - "There is a problem with your configuration in App Store Connect. " + - "More info here: https://errors.rev.cat/configuring-products" + "There is a problem with your configuration in App Store Connect. " + + "More info here: https://errors.rev.cat/configuring-products" case .entitlements_revoked_syncing_purchases(let productIdentifiers): return "Entitlements revoked for product " + - "identifiers: \(productIdentifiers). \nsyncing purchases" + "identifiers: \(productIdentifiers). \nsyncing purchases" case .finishing_transaction(let transaction): return "Finishing transaction \(transaction.payment.productIdentifier) " + - "\(transaction.transactionIdentifier ?? "") " + - "(\(transaction.original?.transactionIdentifier ?? ""))" + "\(transaction.transactionIdentifier ?? "") " + + "(\(transaction.original?.transactionIdentifier ?? ""))" case .purchasing_with_observer_mode_and_finish_transactions_false_warning: return "Observer mode is active (finishTransactions is set to false) and " + - "purchase has been initiated. RevenueCat will not finish the " + - "transaction, are you sure you want to do this?" + "purchase has been initiated. RevenueCat will not finish the " + + "transaction, are you sure you want to do this?" case .paymentqueue_removedtransaction(let transaction): let errorUserInfo = (transaction.error as NSError?)?.userInfo ?? [:] return "PaymentQueue removedTransaction: \(transaction.payment.productIdentifier) " + - "\(transaction.transactionIdentifier ?? "") " + - "(\(transaction.original?.transactionIdentifier ?? "") " + - "\(transaction.error?.localizedDescription ?? "") " + - "\(!errorUserInfo.isEmpty ? errorUserInfo.description : "") - " + - "\(transaction.transactionState.rawValue)" + "\(transaction.transactionIdentifier ?? "") " + + "(\(transaction.original?.transactionIdentifier ?? "") " + + "\(transaction.error?.localizedDescription ?? "") " + + "\(!errorUserInfo.isEmpty ? errorUserInfo.description : "") - " + + "\(transaction.transactionState.rawValue)" case .paymentqueue_revoked_entitlements_for_product_identifiers(let productIdentifiers): return "PaymentQueue " + - "didRevokeEntitlementsForProductIdentifiers: \(productIdentifiers)" + "didRevokeEntitlementsForProductIdentifiers: \(productIdentifiers)" case .paymentqueue_updatedtransaction(let transaction): return "PaymentQueue updatedTransaction: \(transaction.payment.productIdentifier) " + - "\(transaction.transactionIdentifier ?? "") " + - "(\(transaction.error?.localizedDescription ?? "")) " + - "\(transaction.original?.transactionIdentifier ?? "") - " + - "\(transaction.transactionState.rawValue)" + "\(transaction.transactionIdentifier ?? "") " + + "(\(transaction.error?.localizedDescription ?? "")) " + + "\(transaction.original?.transactionIdentifier ?? "") - " + + "\(transaction.transactionState.rawValue)" case .presenting_code_redemption_sheet: return "Presenting code redemption sheet." @@ -99,23 +110,23 @@ extension PurchaseStrings: CustomStringConvertible { case .skpayment_missing_from_skpaymenttransaction: return "There is a problem with the " + - "SKPaymentTransaction missing an SKPayment - this is an issue with the App Store." + "SKPaymentTransaction missing an SKPayment - this is an issue with the App Store." case .skpayment_missing_product_identifier: return "There is a problem with the SKPayment missing " + - "a product identifier - this is an issue with the App Store." + "a product identifier - this is an issue with the App Store." case .could_not_purchase_product_id_not_found: return "makePurchase - Could not purchase SKProduct. " + - "Couldn't find its product identifier. This is possibly an App Store quirk." + "Couldn't find its product identifier. This is possibly an App Store quirk." case .product_identifier_nil: return "Apple returned a product where the productIdentifier is nil, " + - "this is possibly an App Store quirk" + "this is possibly an App Store quirk" case .payment_identifier_nil: return "Apple returned a payment where the productIdentifier is nil, " + - "this is possibly an App Store quirk" + "this is possibly an App Store quirk" case .purchases_nil: return "Purchases has not been configured. Please call Purchases.configure()" @@ -125,13 +136,38 @@ extension PurchaseStrings: CustomStringConvertible { case .purchases_delegate_set_to_nil: return "Purchases delegate is being set to nil, " + - "you probably don't want to do this." + "you probably don't want to do this." case .management_url_nil_opening_default: return "managementURL is nil, opening Apple's subscription management page" - } + case .requested_products_not_found(let request): + return "requested products not found for request: \(request)" + case .callback_not_found_for_request(let request): + return "callback not found for failing request: \(request)" + + case .unable_to_get_intro_eligibility_for_user(let error): + return "Unable to get intro eligibility for appUserID: \(error.localizedDescription)" + case .duplicate_refund_request(let details): + return "Refund already requested for this product and is either pending, already denied, " + + "or already approved: \(details)" + case .failed_refund_request(let details): + return "Refund request submission failed: \(details)" + case .unknown_refund_request_error_type(let details): + return "Unknown RefundRequestError type from the AppStore: \(details)" + case .unknown_refund_request_error(let details): + return "Unknown error type returned from AppStore: \(details)" + case .unknown_refund_request_status: + return "Unknown RefundRequestStatus returned from AppStore" + case .product_unpurchased_or_missing: + return "Product hasn't been purchased or doesn't exist." + case .transaction_unverified(let productID, let errorMessage): + return "Transaction for productID \(productID) is unverified by AppStore. " + + "Verification error \(errorMessage)" + case .begin_refund_request_unsupported: + return "Tried to call beginRefundRequest in a platform that doesn't support it!" + } } } diff --git a/Purchases/Misc/ISOPeriodFormatter.swift b/Purchases/Misc/ISOPeriodFormatter.swift index 5f416ae4b0..90df35f1e1 100644 --- a/Purchases/Misc/ISOPeriodFormatter.swift +++ b/Purchases/Misc/ISOPeriodFormatter.swift @@ -25,7 +25,7 @@ class ISOPeriodFormatter { return stringResult } - private func period(fromUnit unit: SKProduct.PeriodUnit) -> String { + private func period(fromUnit unit: SK1Product.PeriodUnit) -> String { switch unit { case .day: return "D" diff --git a/Purchases/Misc/SystemInfo.swift b/Purchases/Misc/SystemInfo.swift index 94a68e2133..6abf667638 100644 --- a/Purchases/Misc/SystemInfo.swift +++ b/Purchases/Misc/SystemInfo.swift @@ -44,6 +44,7 @@ class SystemInfo { let appleSubscriptionsURL = URL(string: "https://rev.cat/manage-apple-subscription") + let useStoreKit2IfAvailable: Bool var finishTransactions: Bool let platformFlavor: String let platformFlavorVersion: String? @@ -111,7 +112,8 @@ class SystemInfo { init(platformFlavor: String?, platformFlavorVersion: String?, finishTransactions: Bool, - bundle: Bundle = .main) throws { + bundle: Bundle = .main, + useStoreKit2IfAvailable: Bool = false) throws { self.platformFlavor = platformFlavor ?? "native" self.platformFlavorVersion = platformFlavorVersion self.bundle = bundle @@ -123,6 +125,7 @@ class SystemInfo { } self.finishTransactions = finishTransactions + self.useStoreKit2IfAvailable = useStoreKit2IfAvailable } func isApplicationBackgrounded(completion: @escaping (Bool) -> Void) { diff --git a/Purchases/Networking/HTTPClient.swift b/Purchases/Networking/HTTPClient.swift index 1b9f93677a..6dc0564204 100644 --- a/Purchases/Networking/HTTPClient.swift +++ b/Purchases/Networking/HTTPClient.swift @@ -274,7 +274,7 @@ private extension HTTPClient { if JSONSerialization.isValidJSONObject(requestBody) { do { urlRequest.httpBody = try JSONSerialization.data(withJSONObject: requestBody) - } catch let error { + } catch { Logger.error(Strings.network.creating_json_error(requestBody: requestBody, error: error.localizedDescription)) return nil diff --git a/Purchases/Public/EntitlementInfo.swift b/Purchases/Public/EntitlementInfo.swift index 8cb9196b00..a2dcc5cece 100644 --- a/Purchases/Public/EntitlementInfo.swift +++ b/Purchases/Public/EntitlementInfo.swift @@ -306,7 +306,7 @@ private extension EntitlementInfo { return true } - let referenceDate: Date = requestDate ?? Date.init() + let referenceDate: Date = requestDate ?? Date() return expirationDate.timeIntervalSince(referenceDate) > 0 } diff --git a/Purchases/Public/Errors/ErrorCode.swift b/Purchases/Public/Errors/ErrorCode.swift index 3461eba195..0fc039590b 100644 --- a/Purchases/Public/Errors/ErrorCode.swift +++ b/Purchases/Public/Errors/ErrorCode.swift @@ -49,6 +49,12 @@ import Foundation @objc(RCMissingAppUserIDForAliasCreationError) case missingAppUserIDForAliasCreationError = 27 @objc(RCProductDiscountMissingSubscriptionGroupIdentifierError) case productDiscountMissingSubscriptionGroupIdentifierError = 28 + @objc(RCCustomerInfoError) + case customerInfoError = 29 + @objc(RCSystemInfoError) + case systemInfoError = 30 + @objc(RCBeginRefundRequestError) + case beginRefundRequestError = 31 } @@ -128,6 +134,12 @@ extension ErrorCode: DescribableError { return "Unable to create an alias when the alias is either nil or empty string" case .productDiscountMissingSubscriptionGroupIdentifierError: return "Unable to create a discount offer, the product is missing a subscriptionGroupIdentifier." + case .customerInfoError: + return "There was a problem related to the customer info." + case .systemInfoError: + return "There was a problem related to the system info." + case .beginRefundRequestError: + return "Error when trying to begin refund request." @unknown default: return "Something went wrong." } @@ -200,6 +212,12 @@ extension ErrorCode { return "MISSING_APP_USER_ID_FOR_ALIAS_CREATION_ERROR" case .productDiscountMissingSubscriptionGroupIdentifierError: return "PRODUCT_DISCOUNT_MISSING_SUBSCRIPTION_GROUP_IDENTIFIER_ERROR" + case .customerInfoError: + return "CUSTOMER_INFO_ERROR" + case .systemInfoError: + return "SYSTEM_INFO_ERROR" + case .beginRefundRequestError: + return "BEGIN_REFUND_REQUEST_ERROR" @unknown default: return "UNRECOGNIZED_ERROR" } diff --git a/Purchases/Public/IntroEligibility.swift b/Purchases/Public/IntroEligibility.swift index 87c35abe9e..2cafc868c3 100644 --- a/Purchases/Public/IntroEligibility.swift +++ b/Purchases/Public/IntroEligibility.swift @@ -95,10 +95,6 @@ private extension IntroEligibilityStatus { self.status = status } - init(eligibilityStatusCode statusCode: NSNumber) throws { - self.status = try IntroEligibilityStatus(statusCode: statusCode.intValue) - } - @objc private override init() { self.status = .unknown } diff --git a/Purchases/Public/Package.swift b/Purchases/Public/Package.swift index 8f1fec6379..cd00f66934 100644 --- a/Purchases/Public/Package.swift +++ b/Purchases/Public/Package.swift @@ -71,36 +71,27 @@ private extension PackageType { @objc public let identifier: String @objc public let packageType: PackageType - @objc public let product: SKProduct + @objc public let productDetails: ProductDetails @objc public let offeringIdentifier: String @objc public var localizedPriceString: String { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = product.priceLocale - - return formatter.string(from: product.price) ?? "" + return productDetails.localizedPriceString } @objc public var localizedIntroductoryPriceString: String { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *) { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = product.priceLocale - - if let price = product.introductoryPrice?.price { - return formatter.string(from: price) ?? "" - } + // todo: uncomment when product discounts are supported in product details + // return productDetails.localizedIntroductoryPriceString return "" } else { return "" } } - init(identifier: String, packageType: PackageType, product: SKProduct, offeringIdentifier: String) { + init(identifier: String, packageType: PackageType, productDetails: ProductDetails, offeringIdentifier: String) { self.identifier = identifier self.packageType = packageType - self.product = product + self.productDetails = productDetails self.offeringIdentifier = offeringIdentifier } diff --git a/Purchases/Public/Purchases.swift b/Purchases/Public/Purchases.swift index d9bc90df23..9bacb4986f 100644 --- a/Purchases/Public/Purchases.swift +++ b/Purchases/Public/Purchases.swift @@ -158,6 +158,7 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted private let offeringsManager: OfferingsManager private let productsManager: ProductsManager private let customerInfoManager: CustomerInfoManager + private let trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityChecker private let purchasesOrchestrator: PurchasesOrchestrator private let receiptFetcher: ReceiptFetcher private let receiptParser: ReceiptParser @@ -207,7 +208,6 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted let offeringsFactory = OfferingsFactory() let userDefaults = userDefaults ?? UserDefaults.standard let deviceCache = DeviceCache(userDefaults: userDefaults) - let introCalculator = IntroEligibilityCalculator() let receiptParser = ReceiptParser() let customerInfoManager = CustomerInfoManager(operationDispatcher: operationDispatcher, deviceCache: deviceCache, @@ -229,13 +229,18 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted attributionFetcher: attributionFetcher, subscriberAttributesManager: subscriberAttributesManager) let productsRequestFactory = ProductsRequestFactory() - let productsManager = ProductsManager(productsRequestFactory: productsRequestFactory) + let productsManager = ProductsManager(productsRequestFactory: productsRequestFactory, systemInfo: systemInfo) + let introCalculator = IntroEligibilityCalculator(productsManager: productsManager, receiptParser: receiptParser) let offeringsManager = OfferingsManager(deviceCache: deviceCache, operationDispatcher: operationDispatcher, systemInfo: systemInfo, backend: backend, offeringsFactory: offeringsFactory, productsManager: productsManager) + let manageSubsModalHelper = ManageSubscriptionsModalHelper(systemInfo: systemInfo, + customerInfoManager: customerInfoManager, + identityManager: identityManager) + let beginRefundRequestHelper = BeginRefundRequestHelper(systemInfo: systemInfo) let purchasesOrchestrator = PurchasesOrchestrator(productsManager: productsManager, storeKitWrapper: storeKitWrapper, systemInfo: systemInfo, @@ -246,7 +251,15 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted backend: backend, identityManager: identityManager, receiptParser: receiptParser, - deviceCache: deviceCache) + deviceCache: deviceCache, + manageSubscriptionsModalHelper: manageSubsModalHelper, + beginRefundRequestHelper: beginRefundRequestHelper) + let trialOrIntroPriceChecker = TrialOrIntroPriceEligibilityChecker(receiptFetcher: receiptFetcher, + introEligibilityCalculator: introCalculator, + backend: backend, + identityManager: identityManager, + operationDispatcher: operationDispatcher, + productsManager: productsManager) self.init(appUserID: appUserID, requestFetcher: fetcher, receiptFetcher: receiptFetcher, @@ -266,7 +279,8 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted customerInfoManager: customerInfoManager, productsManager: productsManager, offeringsManager: offeringsManager, - purchasesOrchestrator: purchasesOrchestrator) + purchasesOrchestrator: purchasesOrchestrator, + trialOrIntroPriceEligibilityChecker: trialOrIntroPriceChecker) } init(appUserID: String?, @@ -288,7 +302,9 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted customerInfoManager: CustomerInfoManager, productsManager: ProductsManager, offeringsManager: OfferingsManager, - purchasesOrchestrator: PurchasesOrchestrator) { + purchasesOrchestrator: PurchasesOrchestrator, + trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityChecker + ) { Logger.debug(Strings.configure.debug_enabled) Logger.debug(Strings.configure.sdk_version(sdkVersion: Self.frameworkVersion)) @@ -313,6 +329,7 @@ public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompleted self.productsManager = productsManager self.offeringsManager = offeringsManager self.purchasesOrchestrator = purchasesOrchestrator + self.trialOrIntroPriceEligibilityChecker = trialOrIntroPriceEligibilityChecker super.init() @@ -719,11 +736,7 @@ public extension Purchases { */ @objc(purchasePackage:withCompletion:) func purchase(package: Package, completion: @escaping PurchaseCompletedBlock) { - let payment = storeKitWrapper.payment(withProduct: package.product) - purchase(product: package.product, - payment: payment, - presentedOfferingIdentifier: package.offeringIdentifier, - completion: completion) + purchasesOrchestrator.purchase(package: package, completion: completion) } /** @@ -774,8 +787,15 @@ public extension Purchases { @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) @objc(purchasePackage:withDiscount:completion:) func purchase(package: Package, discount: SKPaymentDiscount, completion: @escaping PurchaseCompletedBlock) { - let payment = storeKitWrapper.payment(withProduct: package.product, discount: discount) - purchase(product: package.product, + // todo: add support for SK2 with discounts, move to new class + // https://github.com/RevenueCat/purchases-ios/issues/848 + guard let sk1ProductDetails = package.productDetails as? SK1ProductDetails else { + return + } + let sk1Product = sk1ProductDetails.underlyingSK1Product + let payment = storeKitWrapper.payment(withProduct: sk1Product, + discount: discount) + purchase(product: sk1Product, payment: payment, presentedOfferingIdentifier: package.offeringIdentifier, completion: completion) @@ -833,27 +853,9 @@ public extension Purchases { @objc(checkTrialOrIntroductoryPriceEligibility:completion:) func checkTrialOrIntroductoryPriceEligibility(_ productIdentifiers: [String], completion: @escaping ([String: IntroEligibility]) -> Void) { - receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { maybeData in - if #available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *), - let data = maybeData { - self.modernEligibilityHandler(maybeReceiptData: data, - productIdentifiers: productIdentifiers, - completion: completion) - } else { - self.backend.getIntroEligibility(appUserID: self.appUserID, - receiptData: maybeData ?? Data(), - productIdentifiers: productIdentifiers) { result, maybeError in - if let error = maybeError { - Logger.error(String(format: "Unable to getIntroEligibilityForAppUserID: %@", - error.localizedDescription)) - } - self.operationDispatcher.dispatchOnMainThread { - completion(result) - } - } - } + trialOrIntroPriceEligibilityChecker.checkEligibility(productIdentifiers: productIdentifiers, + completion: completion) } - } /** * Invalidates the cache for purchaser information. @@ -895,54 +897,38 @@ public extension Purchases { purchasesOrchestrator.paymentDiscount(forProductDiscount: discount, product: product, completion: completion) } - // swiftlint:disable line_length - @available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *) - private func modernEligibilityHandler(maybeReceiptData data: Data, - productIdentifiers: [String], - completion receiveEligibility: @escaping ([String: IntroEligibility]) -> Void) { - introEligibilityCalculator - .checkTrialOrIntroductoryPriceEligibility(with: data, - productIdentifiers: Set(productIdentifiers)) { receivedEligibility, maybeError in - if let error = maybeError { - Logger.error(Strings.receipt.parse_receipt_locally_error(error: error)) - self.backend.getIntroEligibility(appUserID: self.appUserID, - receiptData: data, - productIdentifiers: productIdentifiers) { result, maybeAnotherError in - if let intoEligibilityError = maybeAnotherError { - Logger.error(String(format: "Unable to getIntroEligibilityForAppUserID: %@", - intoEligibilityError.localizedDescription)) - } - self.operationDispatcher.dispatchOnMainThread { - receiveEligibility(result) - } - } - } else { - var convertedEligibility: [String: IntroEligibility] = [:] - for (key, value) in receivedEligibility { - do { - let introEligibility = try IntroEligibility(eligibilityStatusCode: value) - convertedEligibility[key] = introEligibility - } catch { - Logger.error(String(format: "Unable to create an RCIntroEligibility: %@", - error.localizedDescription)) - } - } - self.operationDispatcher.dispatchOnMainThread { - receiveEligibility(convertedEligibility) - } - } - } - // swiftlint:enable line_length + /** + * Use this function to open the manage subscriptions modal. + * If the manage subscriptions modal can't be opened, the managementURL in the customerInfo will be opened. + * If managementURL is not available, the App Store's subscription management section will be opened. + * + * - Parameter completion: A completion block that is called when the modal is closed. + * If it was not successful, there will be an `Error`. + */ + @available(iOS 9.0, *) + @available(macOS 10.12, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @objc func showManageSubscriptionModal(completion: @escaping (Error?) -> Void) { + purchasesOrchestrator.showManageSubscriptionModal(completion: completion) } - private func purchase(product: SKProduct, - payment: SKMutablePayment, - presentedOfferingIdentifier: String?, - completion: @escaping PurchaseCompletedBlock) { - purchasesOrchestrator.purchase(product: product, - payment: payment, - presentedOfferingIdentifier: presentedOfferingIdentifier, - completion: completion) + /** + * Presents a refund request sheet in the current window scene for + * the latest transaction associated with the productID + * + * - Parameter productID: The productID to begin a refund request for. + * - Parameter completion: A completion block that is called when the modal is closed. + * If the request was successful, there will be a `RefundRequestStatus`. + * Keep in mind the status could be `userCancelled` + * If the request was unsuccessful, there will be an `Error`. + */ + @available(iOS 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @objc func beginRefundRequest(for productID: String, + completion: @escaping (RefundRequestStatus, Error?) -> Void) { + purchasesOrchestrator.beginRefundRequest(for: productID, completion: completion) } } @@ -1277,4 +1263,14 @@ private extension Purchases { } } + func purchase(product: SKProduct, + payment: SKMutablePayment, + presentedOfferingIdentifier: String?, + completion: @escaping PurchaseCompletedBlock) { + purchasesOrchestrator.purchase(sk1Product: product, + payment: payment, + presentedOfferingIdentifier: presentedOfferingIdentifier, + completion: completion) + } + } diff --git a/Purchases/Purchasing/IntroEligibilityCalculator.swift b/Purchases/Purchasing/IntroEligibilityCalculator.swift index 8cbdc8a2b9..9a9df7dc76 100644 --- a/Purchases/Purchasing/IntroEligibilityCalculator.swift +++ b/Purchases/Purchasing/IntroEligibilityCalculator.swift @@ -20,24 +20,24 @@ class IntroEligibilityCalculator { private let productsManager: ProductsManager private let receiptParser: ReceiptParser - init(productsManager: ProductsManager = ProductsManager(), receiptParser: ReceiptParser = ReceiptParser()) { + init(productsManager: ProductsManager, + receiptParser: ReceiptParser) { self.productsManager = productsManager self.receiptParser = receiptParser } @available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *) - func checkTrialOrIntroductoryPriceEligibility( - with receiptData: Data, - productIdentifiers candidateProductIdentifiers: Set, - completion: @escaping ([String: NSNumber], Error?) -> Void) { + func checkEligibility(with receiptData: Data, + productIdentifiers candidateProductIdentifiers: Set, + completion: @escaping ([String: IntroEligibilityStatus], Error?) -> Void) { guard candidateProductIdentifiers.count > 0 else { completion([:], nil) return } Logger.debug(Strings.customerInfo.checking_intro_eligibility_locally) - var result: [String: NSNumber] = candidateProductIdentifiers.reduce(into: [:]) { resultDict, productId in - resultDict[productId] = IntroEligibilityStatus.unknown.toNSNumber() + var result = candidateProductIdentifiers.reduce(into: [:]) { resultDict, productId in + resultDict[productId] = IntroEligibilityStatus.unknown } do { let receipt = try receiptParser.parse(from: receiptData) @@ -55,7 +55,7 @@ class IntroEligibilityCalculator { candidateProductIdentifiers.contains($0.productIdentifier) } - let eligibility: [String: NSNumber] = self.checkIntroEligibility( + let eligibility = self.checkEligibility( candidateProducts: candidateProducts, purchasedProductsWithIntroOffers: purchasedProductsWithIntroOffersOrFreeTrials) result.merge(eligibility) { (_, new) in new } @@ -65,7 +65,7 @@ class IntroEligibilityCalculator { ) completion(result, nil) } - } catch let error { + } catch { Logger.error(Strings.customerInfo.checking_intro_eligibility_locally_error(error: error)) completion([:], error) return @@ -76,10 +76,15 @@ class IntroEligibilityCalculator { @available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *) private extension IntroEligibilityCalculator { - func checkIntroEligibility(candidateProducts: Set, - purchasedProductsWithIntroOffers: Set) -> [String: NSNumber] { - var result: [String: NSNumber] = [:] + func checkEligibility(candidateProducts: Set, + purchasedProductsWithIntroOffers: Set) -> [String: IntroEligibilityStatus] { + var result: [String: IntroEligibilityStatus] = [:] + for candidate in candidateProducts { + guard candidate.subscriptionPeriod != nil else { + result[candidate.productIdentifier] = IntroEligibilityStatus.unknown + continue + } let usedIntroForProductIdentifier = purchasedProductsWithIntroOffers .contains { purchased in let foundByGroupId = (candidate.subscriptionGroupIdentifier != nil @@ -89,18 +94,10 @@ private extension IntroEligibilityCalculator { let hasIntroductoryPrice = candidate.introductoryPrice != nil result[candidate.productIdentifier] = !hasIntroductoryPrice || usedIntroForProductIdentifier - ? IntroEligibilityStatus.ineligible.toNSNumber() - : IntroEligibilityStatus.eligible.toNSNumber() + ? IntroEligibilityStatus.ineligible + : IntroEligibilityStatus.eligible } return result } } - -extension IntroEligibilityStatus { - - func toNSNumber() -> NSNumber { - return self.rawValue as NSNumber - } - -} diff --git a/Purchases/Purchasing/OfferingsFactory.swift b/Purchases/Purchasing/OfferingsFactory.swift index 19c46911ff..974d062cd2 100644 --- a/Purchases/Purchasing/OfferingsFactory.swift +++ b/Purchases/Purchasing/OfferingsFactory.swift @@ -17,7 +17,7 @@ import StoreKit class OfferingsFactory { - func createOfferings(withProducts products: [String: SKProduct], + func createOfferings(fromProductDetailsByID productDetailsByID: [String: ProductDetails], data: [String: Any]) -> Offerings? { guard let offeringsData = data["offerings"] as? [[String: Any]] else { return nil @@ -25,7 +25,8 @@ class OfferingsFactory { let offerings = offeringsData.reduce([String: Offering]()) { (dict, offeringData) -> [String: Offering] in var dict = dict - if let offering = createOffering(withProducts: products, offeringData: offeringData) { + if let offering = createOffering(fromProductDetailsByID: productDetailsByID, + offeringData: offeringData) { dict[offering.identifier] = offering if offering.availablePackages.isEmpty { Logger.warn(Strings.offering.offering_empty(offeringIdentifier: offering.identifier)) @@ -43,7 +44,7 @@ class OfferingsFactory { return Offerings(offerings: offerings, currentOfferingID: currentOfferingID) } - func createOffering(withProducts products: [String: SKProduct], + func createOffering(fromProductDetailsByID productDetailsByID: [String: ProductDetails], offeringData: [String: Any]) -> Offering? { guard let offeringIdentifier = offeringData["identifier"] as? String, let packagesData = offeringData["packages"] as? [[String: Any]], @@ -52,7 +53,9 @@ class OfferingsFactory { } let availablePackages = packagesData.compactMap { packageData -> Package? in - createPackage(withData: packageData, products: products, offeringIdentifier: offeringIdentifier) + createPackage(withData: packageData, + productDetailsByID: productDetailsByID, + offeringIdentifier: offeringIdentifier) } guard !availablePackages.isEmpty else { return nil @@ -63,10 +66,10 @@ class OfferingsFactory { } func createPackage(withData data: [String: Any], - products: [String: SKProduct], + productDetailsByID: [String: ProductDetails], offeringIdentifier: String) -> Package? { guard let platformProductIdentifier = data["platform_product_identifier"] as? String, - let product = products[platformProductIdentifier], + let product = productDetailsByID[platformProductIdentifier], let identifier = data["identifier"] as? String else { return nil } @@ -74,7 +77,7 @@ class OfferingsFactory { let packageType = Package.packageType(from: identifier) return Package(identifier: identifier, packageType: packageType, - product: product, + productDetails: product, offeringIdentifier: offeringIdentifier) } diff --git a/Purchases/Purchasing/OfferingsManager.swift b/Purchases/Purchasing/OfferingsManager.swift index d56eac933a..4a4e9d8f1c 100644 --- a/Purchases/Purchasing/OfferingsManager.swift +++ b/Purchases/Purchasing/OfferingsManager.swift @@ -56,8 +56,8 @@ class OfferingsManager { systemInfo.isApplicationBackgrounded { isAppBackgrounded in if self.deviceCache.isOfferingsCacheStale(isAppBackgrounded: isAppBackgrounded) { Logger.debug(isAppBackgrounded - ? Strings.offering.offerings_stale_updating_in_background - : Strings.offering.offerings_stale_updating_in_foreground) + ? Strings.offering.offerings_stale_updating_in_background + : Strings.offering.offerings_stale_updating_in_foreground) self.updateOfferingsCache(appUserID: appUserID, isAppBackgrounded: isAppBackgrounded, @@ -83,8 +83,8 @@ class OfferingsManager { } } - internal func getMissingProductIDs(productsFromStore: [String: SKProduct], - productIDsFromBackend: Set) -> Set { + func getMissingProductIDs(productsFromStore: [String: SKProduct], + productIDsFromBackend: Set) -> Set { guard !productIDsFromBackend.isEmpty else { return [] } @@ -105,7 +105,7 @@ private extension OfferingsManager { return } - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsManager.productsFromOptimalStoreKitVersion(withIdentifiers: productIdentifiers) { products in guard !products.isEmpty else { let errorMessage = Strings.offering.configuration_error_skproducts_not_found.description self.handleOfferingsUpdateError(ErrorUtils.configurationError(message: errorMessage), @@ -117,14 +117,9 @@ private extension OfferingsManager { result[product.productIdentifier] = product } - let missingProductIDs = self.getMissingProductIDs(productsFromStore: productsByID, - productIDsFromBackend: productIdentifiers) - if !missingProductIDs.isEmpty { - Logger.appleWarning( - Strings.offering.cannot_find_product_configuration_error(identifiers: missingProductIDs)) - } - - if let createdOfferings = self.offeringsFactory.createOfferings(withProducts: productsByID, data: data) { + if let createdOfferings = self.offeringsFactory.createOfferings(fromProductDetailsByID: productsByID, + data: data) { + self.logMissingProductsIfAppropriate(products: productsByID, offeringsData: data) self.deviceCache.cache(offerings: createdOfferings) self.dispatchCompletionOnMainThreadIfPossible(completion, offerings: createdOfferings, @@ -156,6 +151,20 @@ private extension OfferingsManager { return Set(productIdenfitiersArray) } + func logMissingProductsIfAppropriate(products: [String: ProductDetails], offeringsData: [String: Any]) { + guard !products.isEmpty, + !offeringsData.isEmpty else { + return + } + + let productIdentifiers = extractProductIdentifiers(fromOfferingsData: offeringsData) + let missingProducts = Set(products.keys).intersection(productIdentifiers) + + if !missingProducts.isEmpty { + Logger.appleWarning(Strings.offering.cannot_find_product_configuration_error(identifiers: missingProducts)) + } + } + func dispatchCompletionOnMainThreadIfPossible(_ completion: ((Offerings?, Error?) -> Void)?, offerings: Offerings?, error: Error?) { diff --git a/Purchases/Purchasing/ProductDetails.swift b/Purchases/Purchasing/ProductDetails.swift new file mode 100644 index 0000000000..bbb62bca4d --- /dev/null +++ b/Purchases/Purchasing/ProductDetails.swift @@ -0,0 +1,138 @@ +// +// 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 +// +// opensource.org/licenses/MIT +// +// ProductDetails.swift +// +// Created by Andrés Boedo on 7/16/21. +// + +import Foundation +import StoreKit + +/// TypeAlias to StoreKit 1's Product type, called `StoreKit/SKProduct` +public typealias SK1Product = SKProduct + +/// TypeAlias to StoreKit 2's Product type, called `StoreKit.Product` +@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) +public typealias SK2Product = StoreKit.Product + +/// Abstract class that provides access to all of StoreKit's product type's properties. +@objc(RCProductDetails) public class ProductDetails: NSObject { + public override func isEqual(_ object: Any?) -> Bool { + return self.productIdentifier == (object as? ProductDetails)?.productIdentifier + } + + @objc public var localizedDescription: String { fatalError() } + @objc public var localizedTitle: String { fatalError() } + @objc public var price: Decimal { fatalError() } + @objc public var localizedPriceString: String { fatalError() } + @objc public var productIdentifier: String { fatalError() } + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) + @objc public var isFamilyShareable: Bool { fatalError() } + + @available(iOS 12.0, macCatalyst 13.0, tvOS 12.0, macOS 10.14, watchOS 6.2, *) + @objc public var subscriptionGroupIdentifier: String? { fatalError() } + + // todo: it looks like StoreKit 2 doesn't have support for these? + // YES if this product has content downloadable using SKDownload + // var isDownloadable: Bool { get } + // + // var downloadContentLengths: [NSNumber] { get } + // + // // Version of the downloadable content + // var contentVersion: String { get } + // + // var downloadContentVersion: String { get } + // + + // todo: add subscription period + // https://github.com/RevenueCat/purchases-ios/issues/849 + // @available(iOS 11.2, *) + // var subscriptionPeriod: SKProductSubscriptionPeriod? { get } + // + + // todo: add product discounts + // https://github.com/RevenueCat/purchases-ios/issues/848 + // @available(iOS 11.2, *) + // var introductoryPrice: SKProductDiscount? { get } + // // + // @available(iOS 12.2, *) + // var discounts: [SKProductDiscount] { get } +} + +@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) +@objc(RCSK2ProductDetails) public class SK2ProductDetails: ProductDetails { + + init(sk2Product: SK2Product) { + self._underlyingSK2Product = sk2Product + } + + // We can't directly store instances of StoreKit.Product, since that causes + // linking issues in iOS < 15, even with @available checks correctly in place. + // So instead, we store the underlying product as Any and wrap it with casting. + // https://openradar.appspot.com/radar?id=4970535809187840 + private var _underlyingSK2Product: Any + public var underlyingSK2Product: SK2Product { + // swiftlint:disable:next force_cast + get { _underlyingSK2Product as! SK2Product } + set { _underlyingSK2Product = newValue } + } + + @objc public override var localizedDescription: String { underlyingSK2Product.description } + + @objc public override var price: Decimal { underlyingSK2Product.price } + + @objc public override var localizedPriceString: String { underlyingSK2Product.displayPrice } + + @objc public override var productIdentifier: String { underlyingSK2Product.id } + + @objc public override var isFamilyShareable: Bool { underlyingSK2Product.isFamilyShareable } + + @objc public override var localizedTitle: String { underlyingSK2Product.displayName } + + @objc public override var subscriptionGroupIdentifier: String? { + underlyingSK2Product.subscription?.subscriptionGroupID + } + +} + +@objc(RCSK1ProductDetails) public class SK1ProductDetails: ProductDetails { + + @objc public init(sk1Product: SK1Product) { + self.underlyingSK1Product = sk1Product + } + + @objc public let underlyingSK1Product: SK1Product + + @objc public override var localizedDescription: String { return underlyingSK1Product.localizedDescription } + + @objc public override var price: Decimal { return underlyingSK1Product.price as Decimal } + + @objc public override var localizedPriceString: String { + return formatter.string(from: underlyingSK1Product.price) ?? "" + } + + @objc public override var productIdentifier: String { return underlyingSK1Product.productIdentifier } + + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) + @objc public override var isFamilyShareable: Bool { underlyingSK1Product.isFamilyShareable } + + @objc public override var localizedTitle: String { underlyingSK1Product.localizedTitle } + + @available(iOS 12.0, macCatalyst 13.0, tvOS 12.0, macOS 10.14, watchOS 6.2, *) + override public var subscriptionGroupIdentifier: String? { underlyingSK1Product.subscriptionGroupIdentifier } + + private lazy var formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = underlyingSK1Product.priceLocale + return formatter + }() + +} diff --git a/Purchases/Purchasing/ProductInfoExtractor.swift b/Purchases/Purchasing/ProductInfoExtractor.swift index 861a6f0294..f1c11ac291 100644 --- a/Purchases/Purchasing/ProductInfoExtractor.swift +++ b/Purchases/Purchasing/ProductInfoExtractor.swift @@ -20,7 +20,7 @@ class ProductInfoExtractor { @available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *) private lazy var isoPeriodFormatter = ISOPeriodFormatter() - func extractInfo(from product: SKProduct) -> ProductInfo { + func extractInfo(from product: SK1Product) -> ProductInfo { let paymentMode = extractPaymentMode(for: product) let introPrice = extractIntroPrice(for: product) @@ -51,7 +51,7 @@ class ProductInfoExtractor { private extension ProductInfoExtractor { - func extractIntroDurationType(for product: SKProduct) -> IntroDurationType { + func extractIntroDurationType(for product: SK1Product) -> IntroDurationType { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *), let paymentMode = product.introductoryPrice?.paymentMode { return paymentMode == .freeTrial ? .freeTrial : .introPrice @@ -60,7 +60,7 @@ private extension ProductInfoExtractor { } } - func extractSubscriptionGroup(for product: SKProduct) -> String? { + func extractSubscriptionGroup(for product: SK1Product) -> String? { if #available(iOS 12.0, macOS 10.14.0, tvOS 12.0, *) { return product.subscriptionGroupIdentifier } else { @@ -68,7 +68,7 @@ private extension ProductInfoExtractor { } } - func extractDiscounts(for product: SKProduct) -> [PromotionalOffer]? { + func extractDiscounts(for product: SK1Product) -> [PromotionalOffer]? { if #available(iOS 12.2, macOS 10.14.4, tvOS 12.2, *) { return product.discounts.map(PromotionalOffer.init(withProductDiscount:)) } else { @@ -76,7 +76,7 @@ private extension ProductInfoExtractor { } } - func extractPaymentMode(for product: SKProduct) -> ProductInfo.PaymentMode { + func extractPaymentMode(for product: SK1Product) -> ProductInfo.PaymentMode { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *), let paymentMode = product.introductoryPrice?.paymentMode { return ProductInfo.paymentMode(fromSKProductDiscountPaymentMode: paymentMode) @@ -85,7 +85,7 @@ private extension ProductInfoExtractor { } } - func extractIntroPrice(for product: SKProduct) -> NSDecimalNumber? { + func extractIntroPrice(for product: SK1Product) -> NSDecimalNumber? { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *), let introductoryPrice = product.introductoryPrice { return introductoryPrice.price @@ -94,7 +94,7 @@ private extension ProductInfoExtractor { } } - func extractNormalDuration(for product: SKProduct) -> String? { + func extractNormalDuration(for product: SK1Product) -> String? { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *), let subscriptionPeriod = product.subscriptionPeriod, subscriptionPeriod.numberOfUnits != 0 { @@ -104,7 +104,7 @@ private extension ProductInfoExtractor { } } - func extractIntroDuration(for product: SKProduct) -> String? { + func extractIntroDuration(for product: SK1Product) -> String? { if #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *), let subscriptionPeriod = product.introductoryPrice?.subscriptionPeriod { return isoPeriodFormatter.string(fromProductSubscriptionPeriod: subscriptionPeriod) diff --git a/Purchases/Purchasing/ProductsManager.swift b/Purchases/Purchasing/ProductsManager.swift index 3d856e1397..f35f8bfc82 100644 --- a/Purchases/Purchasing/ProductsManager.swift +++ b/Purchases/Purchasing/ProductsManager.swift @@ -17,146 +17,59 @@ import StoreKit class ProductsManager: NSObject { - private let productsRequestFactory: ProductsRequestFactory - private var cachedProductsByIdentifier: [String: SKProduct] = [:] - private let queue = DispatchQueue(label: "ProductsManager") - private var productsByRequests: [SKRequest: Set] = [:] - private var completionHandlers: [Set: [(Set) -> Void]] = [:] - private let requestTimeoutInSeconds: Int + let productsFetcherSK1: ProductsFetcherSK1 - init(productsRequestFactory: ProductsRequestFactory = ProductsRequestFactory(), - requestTimeoutInSeconds: Int = 30) { - self.productsRequestFactory = productsRequestFactory - self.requestTimeoutInSeconds = requestTimeoutInSeconds + private let systemInfo: SystemInfo + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + private(set) lazy var productsFetcherSK2 = ProductsFetcherSK2() + + init(productsRequestFactory: ProductsRequestFactory = ProductsRequestFactory(), systemInfo: SystemInfo) { + self.productsFetcherSK1 = ProductsFetcherSK1(productsRequestFactory: productsRequestFactory) + self.systemInfo = systemInfo + } + + func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: Set, + completion: @escaping (Set) -> Void) { + + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *), + self.systemInfo.useStoreKit2IfAvailable { + _ = Task { + let productDetails = await self.sk2ProductDetails(withIdentifiers: identifiers) + completion(productDetails) + } + } else { + productsFetcherSK1.products(withIdentifiers: identifiers, completion: completion) + } + } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: Set) async -> Set { + return await withCheckedContinuation { continuation in + productsFromOptimalStoreKitVersion(withIdentifiers: identifiers) { result in + continuation.resume(returning: result) + } + } + } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func sk2ProductDetails(withIdentifiers identifiers: Set) async -> Set { + do { + let productDetails = try await productsFetcherSK2.products(identifiers: identifiers) + return Set(productDetails) + } catch { + Logger.error("Error when fetching SK2 products: \(error.localizedDescription)") + return Set() + } } func products(withIdentifiers identifiers: Set, - completion: @escaping (Set) -> Void) { - guard identifiers.count > 0 else { - completion([]) - return - } - queue.async { [self] in - let productsAlreadyCached = self.cachedProductsByIdentifier.filter { key, _ in identifiers.contains(key) } - if productsAlreadyCached.count == identifiers.count { - let productsAlreadyCachedSet = Set(productsAlreadyCached.values) - Logger.debug(Strings.offering.products_already_cached(identifiers: identifiers)) - completion(productsAlreadyCachedSet) - return - } - - if let existingHandlers = self.completionHandlers[identifiers] { - Logger.debug(Strings.offering.found_existing_product_request(identifiers: identifiers)) - self.completionHandlers[identifiers] = existingHandlers + [completion] - return - } - - Logger.debug( - Strings.offering.no_cached_requests_and_products_starting_skproduct_request(identifiers: identifiers) - ) - let request = self.productsRequestFactory.request(productIdentifiers: identifiers) - request.delegate = self - self.completionHandlers[identifiers] = [completion] - self.productsByRequests[request] = identifiers - self.scheduleCancellationInCaseOfTimeout(for: request) - request.start() - } + completion: @escaping (Set) -> Void) { + return productsFetcherSK1.sk1Products(withIdentifiers: identifiers, completion: completion) } - func cacheProduct(_ product: SKProduct) { - queue.async { - self.cachedProductsByIdentifier[product.productIdentifier] = product - } - } - -} - -extension ProductsManager: SKProductsRequestDelegate { - - func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - queue.async { [self] in - Logger.rcSuccess(Strings.storeKit.skproductsrequest_received_response) - guard let requestProducts = self.productsByRequests[request] else { - Logger.error("requested products not found for request: \(request)") - return - } - guard let completionBlocks = self.completionHandlers[requestProducts] else { - Logger.error("callback not found for failing request: \(request)") - return - } - - self.completionHandlers.removeValue(forKey: requestProducts) - self.productsByRequests.removeValue(forKey: request) - - self.cacheProducts(response.products) - for completion in completionBlocks { - completion(Set(response.products)) - } - } - } - - func requestDidFinish(_ request: SKRequest) { - Logger.rcSuccess(Strings.storeKit.skproductsrequest_finished) - request.cancel() - } - - func request(_ request: SKRequest, didFailWithError error: Error) { - queue.async { [self] in - Logger.appleError(Strings.storeKit.skproductsrequest_failed(error: error)) - guard let products = self.productsByRequests[request] else { - Logger.error("requested products not found for request: \(request)") - return - } - guard let completionBlocks = self.completionHandlers[products] else { - Logger.error("callback not found for failing request: \(request)") - return - } - - self.completionHandlers.removeValue(forKey: products) - self.productsByRequests.removeValue(forKey: request) - for completion in completionBlocks { - completion(Set()) - } - } - request.cancel() - } - -} - -private extension ProductsManager { - - func cacheProducts(_ products: [SKProduct]) { - let productsByIdentifier = products.reduce(into: [:]) { resultDict, product in - resultDict[product.productIdentifier] = product - } - - cachedProductsByIdentifier = cachedProductsByIdentifier.merging(productsByIdentifier) - } - - // Even though there's a specific delegate method for when SKProductsRequest fails, - // there seem to be some situations in which SKProductsRequest hangs forever, - // without timing out and calling the delegate. - // So we schedule a cancellation just in case, and skip it if all goes as expected. - // More information: https://rev.cat/skproductsrequest-hangs - func scheduleCancellationInCaseOfTimeout(for request: SKProductsRequest) { - queue.asyncAfter(deadline: .now() + .seconds(requestTimeoutInSeconds)) { [weak self] in - guard let self = self, - let products = self.productsByRequests[request] else { return } - - request.cancel() - - Logger.appleError(Strings.storeKit.skproductsrequest_timed_out(after: self.requestTimeoutInSeconds)) - guard let completionBlocks = self.completionHandlers[products] else { - Logger.error("callback not found for failing request: \(request)") - return - } - - self.completionHandlers.removeValue(forKey: products) - self.productsByRequests.removeValue(forKey: request) - for completion in completionBlocks { - completion(Set()) - } - } + func cacheProduct(_ product: SK1Product) { + productsFetcherSK1.cacheProduct(product) } } diff --git a/Purchases/Purchasing/PurchasesOrchestrator.swift b/Purchases/Purchasing/PurchasesOrchestrator.swift index 612ef7d6a5..e776d67eba 100644 --- a/Purchases/Purchasing/PurchasesOrchestrator.swift +++ b/Purchases/Purchasing/PurchasesOrchestrator.swift @@ -16,7 +16,8 @@ import StoreKit @objc protocol PurchasesOrchestratorDelegate { - func shouldPurchasePromoProduct(_ product: SKProduct, defermentBlock: @escaping DeferredPromotionalPurchaseBlock) + func shouldPurchasePromoProduct(_ product: SK1Product, + defermentBlock: @escaping DeferredPromotionalPurchaseBlock) } @@ -54,8 +55,13 @@ class PurchasesOrchestrator { private let identityManager: IdentityManager private let receiptParser: ReceiptParser private let deviceCache: DeviceCache + private let manageSubscriptionsModalHelper: ManageSubscriptionsModalHelper + private let beginRefundRequestHelper: BeginRefundRequestHelper private let lock = NSRecursiveLock() + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + lazy var storeKit2Listener = StoreKit2TransactionListener(delegate: self) + init(productsManager: ProductsManager, storeKitWrapper: StoreKitWrapper, systemInfo: SystemInfo, @@ -66,7 +72,9 @@ class PurchasesOrchestrator { backend: Backend, identityManager: IdentityManager, receiptParser: ReceiptParser, - deviceCache: DeviceCache) { + deviceCache: DeviceCache, + manageSubscriptionsModalHelper: ManageSubscriptionsModalHelper, + beginRefundRequestHelper: BeginRefundRequestHelper) { self.productsManager = productsManager self.storeKitWrapper = storeKitWrapper self.systemInfo = systemInfo @@ -78,6 +86,11 @@ class PurchasesOrchestrator { self.identityManager = identityManager self.receiptParser = receiptParser self.deviceCache = deviceCache + self.manageSubscriptionsModalHelper = manageSubscriptionsModalHelper + self.beginRefundRequestHelper = beginRefundRequestHelper + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + storeKit2Listener.listenForTransactions() + } } func restoreTransactions(completion maybeCompletion: ((CustomerInfo?, Error?) -> Void)?) { @@ -90,7 +103,7 @@ class PurchasesOrchestrator { maybeCompletion: maybeCompletion) } - func products(withIdentifiers identifiers: [String], completion: @escaping ([SKProduct]) -> Void) { + func products(withIdentifiers identifiers: [String], completion: @escaping ([SK1Product]) -> Void) { let productIdentifiersSet = Set(identifiers) guard !productIdentifiersSet.isEmpty else { operationDispatcher.dispatchOnMainThread { completion([]) } @@ -104,9 +117,24 @@ class PurchasesOrchestrator { } } + func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: [String], + completion: @escaping ([ProductDetails]) -> Void) { + let productIdentifiersSet = Set(identifiers) + guard !productIdentifiersSet.isEmpty else { + operationDispatcher.dispatchOnMainThread { completion([]) } + return + } + + productsManager.productsFromOptimalStoreKitVersion(withIdentifiers: productIdentifiersSet) { products in + self.operationDispatcher.dispatchOnMainThread { + completion(Array(products)) + } + } + } + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func paymentDiscount(forProductDiscount productDiscount: SKProductDiscount, - product: SKProduct, + product: SK1Product, completion: @escaping (SKPaymentDiscount?, Error?) -> Void) { guard let discountIdentifier = productDiscount.identifier else { completion(nil, ErrorUtils.productDiscountMissingIdentifierError()) @@ -162,12 +190,40 @@ class PurchasesOrchestrator { } } - func purchase(product: SKProduct, + func purchase(package: Package, completion: @escaping PurchaseCompletedBlock) { + // todo: clean up, move to new class along with the private funcs below + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *), + package.productDetails is SK2ProductDetails { + _ = Task { + let result = await purchase(sk2Package: package) + DispatchQueue.main.async { + switch result { + case .failure(let error): + completion(nil, nil, error, false) + case .success(let (customerInfo, userCancelled)): + // todo: change API and send transaction + if userCancelled { + completion(nil, nil, ErrorUtils.purchaseCancelledError(), userCancelled) + } else { + completion(nil, customerInfo, nil, userCancelled) + } + } + } + } + } else { + guard package.productDetails is SK1ProductDetails else { + fatalError("could not identify StoreKit version to use! ProductDetails: \(package.productDetails)") + } + purchase(sk1Package: package, completion: completion) + } + } + + func purchase(sk1Product: SK1Product, payment: SKMutablePayment, presentedOfferingIdentifier maybePresentedOfferingIdentifier: String?, completion: @escaping PurchaseCompletedBlock) { Logger.debug(String(format: "Make purchase called: %@", #function)) - guard let productIdentifier = extractProductIdentifier(fromProduct: product, orPayment: payment) else { + guard let productIdentifier = extractProductIdentifier(fromProduct: sk1Product, orPayment: payment) else { Logger.error(Strings.purchase.could_not_purchase_product_id_not_found) let errorMessage = "There was a problem purchasing the product: productIdentifier was nil" completion(nil, nil, ErrorUtils.unknownError(message: errorMessage), false) @@ -196,7 +252,7 @@ class PurchasesOrchestrator { Logger.purchase(Strings.purchase.purchasing_product(productIdentifier: productIdentifier)) } - productsManager.cacheProduct(product) + productsManager.cacheProduct(sk1Product) lock.lock() defer { @@ -211,6 +267,35 @@ class PurchasesOrchestrator { storeKitWrapper.add(payment) } + @available(iOS 9.0, *) + @available(macOS 10.12, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + func showManageSubscriptionModal(completion: @escaping (Error?) -> Void) { + self.manageSubscriptionsModalHelper.showManageSubscriptionModal { result in + switch result { + case .failure(let error): + completion(error) + case .success: + completion(nil) + } + } + } + + @available(iOS 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + func beginRefundRequest(for productID: String, completion: @escaping (RefundRequestStatus, Error?) -> Void) { + beginRefundRequestHelper.beginRefundRequest(productID: productID) { result in + switch result { + case .failure(let error): + completion(.error, error) + case .success(let status): + completion(status, nil) + } + } + } + } extension PurchasesOrchestrator: StoreKitWrapperDelegate { @@ -238,7 +323,7 @@ extension PurchasesOrchestrator: StoreKitWrapperDelegate { func storeKitWrapper(_ storeKitWrapper: StoreKitWrapper, shouldAddStorePayment payment: SKPayment, - for product: SKProduct) -> Bool { + for product: SK1Product) -> Bool { productsManager.cacheProduct(product) guard let delegate = maybeDelegate else { return false } @@ -315,6 +400,15 @@ private extension PurchasesOrchestrator { } +extension PurchasesOrchestrator: StoreKit2TransactionListenerDelegate { + + func transactionsUpdated() { + // todo: should isRestore here be set to observer mode? + syncPurchases(receiptRefreshPolicy: .always, isRestore: false, maybeCompletion: nil) + } + +} + // MARK: Private funcs. private extension PurchasesOrchestrator { @@ -346,7 +440,9 @@ private extension PurchasesOrchestrator { } } - func postReceipt(withTransaction transaction: SKPaymentTransaction, receiptData: Data, products: Set) { + func postReceipt(withTransaction transaction: SKPaymentTransaction, + receiptData: Data, + products: Set) { var maybeProductInfo: ProductInfo? var maybePresentedOfferingID: String? if let product = products.first { @@ -495,10 +591,10 @@ private extension PurchasesOrchestrator { } } - // Although both SKProduct.productIdentifier and SKPayment.productIdentifier + // Although both SK1Product.productIdentifier and SKPayment.productIdentifier // are supposed to be non-null, we've seen instances where this is not true. // so we cast into optionals in order to check nullability, and try to fall back if possible. - func extractProductIdentifier(fromProduct product: SKProduct, orPayment payment: SKPayment) -> String? { + func extractProductIdentifier(fromProduct product: SK1Product, orPayment payment: SKPayment) -> String? { if let identifierFromProduct = product.productIdentifier as String?, !identifierFromProduct.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return identifierFromProduct @@ -519,6 +615,49 @@ private extension PurchasesOrchestrator { deviceCache.setOfferingsCacheTimestampToNow() } + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func purchase(sk2Package: Package) async -> Result<(CustomerInfo, Bool), Error> { + guard let sk2ProductDetails = sk2Package.productDetails as? SK2ProductDetails else { + // todo: use custom error + return .failure(ErrorUtils.unexpectedBackendResponseError()) + } + + let sk2Product = sk2ProductDetails.underlyingSK2Product + do { + let result = try await sk2Product.purchase() + let userCancelled = await storeKit2Listener.handle(purchaseResult: result) + + return await withCheckedContinuation { continuation in + syncPurchases(receiptRefreshPolicy: .always, isRestore: false) { maybeCustomerInfo, maybeError in + if let error = maybeError { + continuation.resume(returning: .failure(error)) + return + } + guard let customerInfo = maybeCustomerInfo else { + continuation.resume(returning: .failure(ErrorUtils.unexpectedBackendResponseError())) + return + } + + continuation.resume(returning: .success((customerInfo, userCancelled))) + } + } + } catch { + return .failure(error) + } + } + + func purchase(sk1Package: Package, completion: @escaping PurchaseCompletedBlock) { + guard let sk1ProductDetails = sk1Package.productDetails as? SK1ProductDetails else { + return + } + let sk1Product = sk1ProductDetails.underlyingSK1Product + let payment = storeKitWrapper.payment(withProduct: sk1Product) + purchase(sk1Product: sk1Product, + payment: payment, + presentedOfferingIdentifier: sk1Package.offeringIdentifier, + completion: completion) + } + } // swiftlint:disable:this file_length diff --git a/Purchases/Purchasing/StoreKit1/ProductsFetcherSK1.swift b/Purchases/Purchasing/StoreKit1/ProductsFetcherSK1.swift new file mode 100644 index 0000000000..5f7eadabc6 --- /dev/null +++ b/Purchases/Purchasing/StoreKit1/ProductsFetcherSK1.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 +// +// ProductsFetcherSK1.swift +// +// Created by Andrés Boedo on 7/26/21. + +import Foundation +import StoreKit + +class ProductsFetcherSK1: NSObject { + + private let productsRequestFactory: ProductsRequestFactory + + private var cachedProductsByIdentifier: [String: SK1Product] = [:] + private let queue = DispatchQueue(label: "ProductsFetcherSK1") + private var productsByRequests: [SKRequest: Set] = [:] + private var completionHandlers: [Set: [(Set) -> Void]] = [:] + private let requestTimeoutInSeconds: Int + + init(productsRequestFactory: ProductsRequestFactory = ProductsRequestFactory(), + requestTimeoutInSeconds: Int = 30) { + self.productsRequestFactory = productsRequestFactory + self.requestTimeoutInSeconds = requestTimeoutInSeconds + } + + func sk1Products(withIdentifiers identifiers: Set, + completion: @escaping (Set) -> Void) { + guard identifiers.count > 0 else { + completion([]) + return + } + queue.async { [self] in + let productsAlreadyCached = self.cachedProductsByIdentifier.filter { key, _ in identifiers.contains(key) } + if productsAlreadyCached.count == identifiers.count { + let productsAlreadyCachedSet = Set(productsAlreadyCached.values) + Logger.debug(Strings.offering.products_already_cached(identifiers: identifiers)) + completion(productsAlreadyCachedSet) + return + } + + if let existingHandlers = self.completionHandlers[identifiers] { + Logger.debug(Strings.offering.found_existing_product_request(identifiers: identifiers)) + self.completionHandlers[identifiers] = existingHandlers + [completion] + return + } + + Logger.debug( + Strings.offering.no_cached_requests_and_products_starting_skproduct_request(identifiers: identifiers) + ) + let request = self.productsRequestFactory.request(productIdentifiers: identifiers) + request.delegate = self + self.completionHandlers[identifiers] = [completion] + self.productsByRequests[request] = identifiers + request.start() + scheduleCancellationInCaseOfTimeout(for: request) + } + } + + func products(withIdentifiers identifiers: Set, + completion: @escaping (Set) -> Void) { + self.sk1Products(withIdentifiers: identifiers) { skProducts in + let wrappedProductsArray = skProducts.map { SK1ProductDetails(sk1Product: $0) } + completion(Set(wrappedProductsArray)) + } + } + + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func products(withIdentifiers identifiers: Set) async -> Set { + return await withCheckedContinuation { continuation in + products(withIdentifiers: identifiers) { result in + continuation.resume(returning: result) + } + } + } + +} + +extension ProductsFetcherSK1: SKProductsRequestDelegate { + + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + queue.async { [self] in + Logger.rcSuccess(Strings.storeKit.skproductsrequest_received_response) + guard let requestProducts = self.productsByRequests[request] else { + Logger.error("requested products not found for request: \(request)") + return + } + guard let completionBlocks = self.completionHandlers[requestProducts] else { + Logger.error("callback not found for failing request: \(request)") + self.productsByRequests.removeValue(forKey: request) + return + } + + self.completionHandlers.removeValue(forKey: requestProducts) + self.productsByRequests.removeValue(forKey: request) + + self.cacheProducts(response.products) + for completion in completionBlocks { + completion(Set(response.products)) + } + } + } + + func requestDidFinish(_ request: SKRequest) { + Logger.rcSuccess(Strings.storeKit.skproductsrequest_finished) + self.cancelRequestToPreventTimeoutWarnings(request) + } + + func request(_ request: SKRequest, didFailWithError error: Error) { + queue.async { [self] in + Logger.appleError(Strings.storeKit.skproductsrequest_failed(error: error)) + guard let products = self.productsByRequests[request] else { + Logger.error(Strings.purchase.requested_products_not_found(request: request)) + return + } + guard let completionBlocks = self.completionHandlers[products] else { + Logger.error(Strings.purchase.callback_not_found_for_request(request: request)) + self.productsByRequests.removeValue(forKey: request) + return + } + + self.completionHandlers.removeValue(forKey: products) + self.productsByRequests.removeValue(forKey: request) + for completion in completionBlocks { + completion(Set()) + } + } + self.cancelRequestToPreventTimeoutWarnings(request) + } + + func cacheProduct(_ product: SK1Product) { + queue.async { + self.cachedProductsByIdentifier[product.productIdentifier] = product + } + } +} + +private extension ProductsFetcherSK1 { + + func cacheProducts(_ products: [SK1Product]) { + queue.async { + let productsByIdentifier = products.reduce(into: [:]) { resultDict, product in + resultDict[product.productIdentifier] = product + } + + self.cachedProductsByIdentifier += productsByIdentifier + } + } + + // Even though the request has finished, we've seen instances where + // the request seems to live on. So we manually call `cancel` to prevent warnings in runtime. + // https://github.com/RevenueCat/purchases-ios/issues/250 + // https://github.com/RevenueCat/purchases-ios/issues/391 + func cancelRequestToPreventTimeoutWarnings(_ request: SKRequest) { + request.cancel() + } + + // Even though there's a specific delegate method for when SKProductsRequest fails, + // there seem to be some situations in which SKProductsRequest hangs forever, + // without timing out and calling the delegate. + // So we schedule a cancellation just in case, and skip it if all goes as expected. + // More information: https://rev.cat/skproductsrequest-hangs + func scheduleCancellationInCaseOfTimeout(for request: SKProductsRequest) { + queue.asyncAfter(deadline: .now() + .seconds(requestTimeoutInSeconds)) { [weak self] in + guard let self = self, + let products = self.productsByRequests[request] else { return } + + request.cancel() + + Logger.appleError(Strings.storeKit.skproductsrequest_timed_out(after: self.requestTimeoutInSeconds)) + guard let completionBlocks = self.completionHandlers[products] else { + Logger.error("callback not found for failing request: \(request)") + return + } + + self.completionHandlers.removeValue(forKey: products) + self.productsByRequests.removeValue(forKey: request) + for completion in completionBlocks { + completion(Set()) + } + } + } + +} diff --git a/Purchases/StoreKitExtensions/SKPaymentTransaction+Extensions.swift b/Purchases/Purchasing/StoreKit1/StoreKitExtensions/SKPaymentTransaction+Extensions.swift similarity index 100% rename from Purchases/StoreKitExtensions/SKPaymentTransaction+Extensions.swift rename to Purchases/Purchasing/StoreKit1/StoreKitExtensions/SKPaymentTransaction+Extensions.swift diff --git a/Purchases/Purchasing/StoreKitRequestFetcher.swift b/Purchases/Purchasing/StoreKit1/StoreKitRequestFetcher.swift similarity index 100% rename from Purchases/Purchasing/StoreKitRequestFetcher.swift rename to Purchases/Purchasing/StoreKit1/StoreKitRequestFetcher.swift diff --git a/Purchases/Purchasing/StoreKitWrapper.swift b/Purchases/Purchasing/StoreKit1/StoreKitWrapper.swift similarity index 93% rename from Purchases/Purchasing/StoreKitWrapper.swift rename to Purchases/Purchasing/StoreKit1/StoreKitWrapper.swift index 5651b77d73..5746f36727 100644 --- a/Purchases/Purchasing/StoreKitWrapper.swift +++ b/Purchases/Purchasing/StoreKit1/StoreKitWrapper.swift @@ -22,7 +22,7 @@ protocol StoreKitWrapperDelegate: AnyObject { func storeKitWrapper(_ storeKitWrapper: StoreKitWrapper, shouldAddStorePayment payment: SKPayment, - for product: SKProduct) -> Bool + for product: SK1Product) -> Bool func storeKitWrapper(_ storeKitWrapper: StoreKitWrapper, didRevokeEntitlementsForProductIdentifiers productIdentifiers: [String]) @@ -76,7 +76,7 @@ class StoreKitWrapper: NSObject, SKPaymentTransactionObserver { paymentQueue.presentCodeRedemptionSheet() } - func payment(withProduct product: SKProduct) -> SKMutablePayment { + func payment(withProduct product: SK1Product) -> SKMutablePayment { let payment = SKMutablePayment(product: product) if #available(macOS 10.14, watchOS 6.2, macCatalyst 13.0, *) { @@ -86,7 +86,7 @@ class StoreKitWrapper: NSObject, SKPaymentTransactionObserver { } @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) - func payment(withProduct product: SKProduct, discount: SKPaymentDiscount) -> SKMutablePayment { + func payment(withProduct product: SK1Product, discount: SKPaymentDiscount) -> SKMutablePayment { let payment = self.payment(withProduct: product) payment.paymentDiscount = discount return payment @@ -116,7 +116,7 @@ extension StoreKitWrapper: SKPaymentQueueDelegate { @available(watchOS, unavailable) func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, - for product: SKProduct) -> Bool { + for product: SK1Product) -> Bool { return delegate?.storeKitWrapper(self, shouldAddStorePayment: payment, for: product) ?? false } diff --git a/Purchases/Purchasing/StoreKit2/ProductsFetcherSK2.swift b/Purchases/Purchasing/StoreKit2/ProductsFetcherSK2.swift new file mode 100644 index 0000000000..e3b52ba5e5 --- /dev/null +++ b/Purchases/Purchasing/StoreKit2/ProductsFetcherSK2.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 +// +// ProductsManagerSK2.swift +// +// Created by Andrés Boedo on 7/23/21. + +import Foundation +import StoreKit + +enum ProductsManagerSK2Error: Error { + + case productsRequestError(innerError: Error) + +} + +@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) +actor ProductsFetcherSK2 { + + private var cachedProductsByIdentifier: [String: SK2ProductDetails] = [:] + + func products(identifiers: Set) async throws -> Set { + do { + // todo: also cache requests, so that if a request is in flight for the same products, + // we don't need to make a new one + let productsAlreadyCached = self.cachedProductsByIdentifier.filter { key, _ in identifiers.contains(key) } + if productsAlreadyCached.count == identifiers.count { + let productsAlreadyCachedSet = Set(productsAlreadyCached.values) + Logger.debug(Strings.offering.products_already_cached(identifiers: identifiers)) + return productsAlreadyCachedSet + } + + let storeKitProducts = try await StoreKit.Product.products(for: identifiers) + let sk2ProductDetails = storeKitProducts.map { SK2ProductDetails(sk2Product: $0) } + return Set(sk2ProductDetails) + } catch { + throw ProductsManagerSK2Error.productsRequestError(innerError: error) + } + } + +} diff --git a/Purchases/Purchasing/StoreKit2/SK2BeginRefundRequestHelper.swift b/Purchases/Purchasing/StoreKit2/SK2BeginRefundRequestHelper.swift new file mode 100644 index 0000000000..bf6ac6f4ac --- /dev/null +++ b/Purchases/Purchasing/StoreKit2/SK2BeginRefundRequestHelper.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 +// +// SK2BeginRefundRequestHelper.swift +// +// Created by Madeline Beyl on 10/21/21. + +#if os(iOS) || targetEnvironment(macCatalyst) +import Foundation +import StoreKit +import UIKit + +/// Helper class responsible for calling into StoreKit2 and translating results/errors for consumption by RevenueCat. +@available(iOS 15.0, *) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +class SK2BeginRefundRequestHelper { + + /// Calls `initiateSK2RefundRequest` and maps the result for consumption by `BeginRefundRequestHelper` + @MainActor + func initiateRefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async -> + Result { + let sk2Result = await initiateSK2RefundRequest(transactionID: transactionID, windowScene: windowScene) + return mapSk2Result(from: sk2Result) + } + + /* Checks with StoreKit2 that the given `productID` has an existing verified transaction, and maps the + * result for consumption by `BeginRefundRequestHelper`. + */ + func verifyTransaction(productID: String) async -> Result { + let maybeResult = await StoreKit.Transaction.latest(for: productID) + guard let nonNilResult = maybeResult else { + let errorMessage = Strings.purchase.product_unpurchased_or_missing.description + return .failure(ErrorUtils.beginRefundRequestError(withMessage: errorMessage)) + } + + switch nonNilResult { + case .unverified(_, let verificationError): + let message = Strings.purchase.transaction_unverified( + productID: productID, + errorMessage: verificationError.localizedDescription).description + return .failure(ErrorUtils.beginRefundRequestError(withMessage: message)) + case .verified(let transaction): return .success(transaction.id) + } + } + + /* + * Attempts to begin a refund request for the given transactionID and current windowScene with StoreKit2. + * If successful, passes result on as-is. If unsuccessful, calls `getErrorMessage` to add more + * descriptive details to the error. + * + * This function allows for us to mock the StoreKit2 response in unit tests. + */ + @MainActor + func initiateSK2RefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async -> + Result { + do { + let sk2Status = try await StoreKit.Transaction.beginRefundRequest(for: transactionID, in: windowScene) + return .success(sk2Status) + } catch { + let message = getErrorMessage(from: error) + return .failure(ErrorUtils.beginRefundRequestError(withMessage: message, error: error)) + } + } + +} + +@available(iOS 15.0, *) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +private extension SK2BeginRefundRequestHelper { + + func getErrorMessage(from maybeSK2Error: Error?) -> String { + let details = maybeSK2Error?.localizedDescription ?? "No extra info" + if let skError = maybeSK2Error as? StoreKit.Transaction.RefundRequestError { + switch skError { + case .duplicateRequest: + return Strings.purchase.duplicate_refund_request(details: details).description + case .failed: + return Strings.purchase.failed_refund_request(details: details).description + @unknown default: + return Strings.purchase.unknown_refund_request_error_type(details: details).description + } + } else { + return Strings.purchase.unknown_refund_request_error(details: details).description + } + } + + /* + * - Parameter sk2Result: The Result returned from StoreKit2 + * - Returns The result expected by `BeginRefundRequestHelper`, converting from a StoreKit RefundRequestStatus + * to our `RefundRequestStatus` type and adding more descriptive error messages where needed. + */ + func mapSk2Result(from sk2Result: Result) -> + Result { + switch sk2Result { + case .success(let sk2Status): + guard let rcStatus = RefundRequestStatus.from(sk2RefundRequestStatus: sk2Status) else { + return .failure(ErrorUtils.beginRefundRequestError( + withMessage: Strings.purchase.unknown_refund_request_status.description)) + } + return .success(rcStatus) + case .failure(let error): + let message = getErrorMessage(from: error) + return .failure(ErrorUtils.beginRefundRequestError(withMessage: message)) + } + } + +} + +@available(iOS 15.0, *) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +private extension RefundRequestStatus { + + static func from(sk2RefundRequestStatus status: StoreKit.Transaction.RefundRequestStatus) -> RefundRequestStatus? { + switch status { + case .userCancelled: + return .userCancelled + case .success: + return .success + @unknown default: + return nil + } + } + +} +#endif diff --git a/Purchases/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Purchases/Purchasing/StoreKit2/StoreKit2TransactionListener.swift new file mode 100644 index 0000000000..4b52d428b5 --- /dev/null +++ b/Purchases/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -0,0 +1,86 @@ +// +// 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 +// +// StoreKit2TransactionListener.swift +// +// Created by Andrés Boedo on 31/8/21. + +import Foundation +import StoreKit + +protocol StoreKit2TransactionListenerDelegate: AnyObject { + func transactionsUpdated() +} + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +class StoreKit2TransactionListener { + + private var taskHandle: Task? + weak var delegate: StoreKit2TransactionListenerDelegate? + + init(delegate: StoreKit2TransactionListenerDelegate?) { + self.delegate = delegate + } + + func listenForTransactions() { + self.taskHandle = Task { + for await result in StoreKit.Transaction.updates { + await handle(transactionResult: result) + } + } + } + + func handle(purchaseResult: StoreKit.Product.PurchaseResult) async -> Bool { + switch purchaseResult { + case .success(let verificationResult): + await handle(transactionResult: verificationResult) + // todo: proper handling + return false + case .pending: + Logger.info("the transaction is pending") + // todo: proper handling + return false + case .userCancelled: + Logger.info("the transaction is cancelled") + return true + @unknown default: + // todo: proper handling + Logger.info("") + return false + } + } + +} + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +private extension StoreKit2TransactionListener { + + func handle(transactionResult: VerificationResult) async { + switch transactionResult { + case .unverified(let unverifiedTransaction, let verificationError): + // todo: update once StoreKit fixes the issue with verifying sandbox purchases. + #if DEBUG + Logger.debug("The transaction has failed verification, but Sandbox purchases dont' support" + + "verification as of Beta 8. Details: \(verificationError.localizedDescription)") + await finish(transaction: unverifiedTransaction) + #else + Logger.error("StoreKit has parsed the JWS but failed verification. The content will not be" + + "made available to the user. Details: \(verificationError.localizedDescription)") + #endif + case .verified(let verifiedTransaction): + await finish(transaction: verifiedTransaction) + } + } + + func finish(transaction: StoreKit.Transaction) async { + await transaction.finish() + delegate?.transactionsUpdated() + } + +} diff --git a/Purchases/Purchasing/TrialOrIntroPriceEligibilityChecker.swift b/Purchases/Purchasing/TrialOrIntroPriceEligibilityChecker.swift new file mode 100644 index 0000000000..1501efdcd6 --- /dev/null +++ b/Purchases/Purchasing/TrialOrIntroPriceEligibilityChecker.swift @@ -0,0 +1,144 @@ +// +// 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 +// +// IntroTrialOrIntroductoryPriceEligibilityChecker.swift +// +// Created by César de la Vega on 8/31/21. + +import Foundation +import StoreKit + +class TrialOrIntroPriceEligibilityChecker { + + typealias ReceiveIntroEligibilityBlock = ([String: IntroEligibility]) -> Void + private var appUserID: String { identityManager.currentAppUserID } + private let receiptFetcher: ReceiptFetcher + private let introEligibilityCalculator: IntroEligibilityCalculator + private let backend: Backend + private let identityManager: IdentityManager + private let operationDispatcher: OperationDispatcher + private let productsManager: ProductsManager + + init(receiptFetcher: ReceiptFetcher, + introEligibilityCalculator: IntroEligibilityCalculator, + backend: Backend, + identityManager: IdentityManager, + operationDispatcher: OperationDispatcher, + productsManager: ProductsManager) { + self.receiptFetcher = receiptFetcher + self.introEligibilityCalculator = introEligibilityCalculator + self.backend = backend + self.identityManager = identityManager + self.operationDispatcher = operationDispatcher + self.productsManager = productsManager + } + + func checkEligibility(productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) { + _ = Task { + let eligibility = await sk2CheckEligibility(productIdentifiers) + completion(eligibility) + } + } else { + sk1CheckEligibility(productIdentifiers, completion: completion) + } + } + + func sk1CheckEligibility(_ productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { maybeData in + if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.2, *), + let data = maybeData { + self.sk1CheckEligibility(with: data, + productIdentifiers: productIdentifiers, + completion: completion) + } else { + self.fetchIntroEligibilityFromBackend(with: maybeData ?? Data(), + productIdentifiers: productIdentifiers, + completion: completion) + } + } + } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func sk2CheckEligibility(_ productIdentifiers: [String]) async -> [String: IntroEligibility] { + let identifiers = Set(productIdentifiers) + var introDict = productIdentifiers.reduce(into: [:]) { resultDict, productId in + resultDict[productId] = IntroEligibility(eligibilityStatus: IntroEligibilityStatus.unknown) + } + let products = await productsManager.sk2ProductDetails(withIdentifiers: identifiers) + for sk2ProductDetails in products { + let sk2Product = sk2ProductDetails.underlyingSK2Product + let maybeIsEligible = await sk2Product.subscription?.isEligibleForIntroOffer + + let eligibilityStatus: IntroEligibilityStatus + + if let isEligible = maybeIsEligible { + eligibilityStatus = isEligible ? .eligible : .ineligible + } else { + eligibilityStatus = .unknown + } + + introDict[sk2ProductDetails.productIdentifier] = + IntroEligibility(eligibilityStatus: eligibilityStatus) + } + return introDict + } + +} + +fileprivate extension TrialOrIntroPriceEligibilityChecker { + + @available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.2, *) + func sk1CheckEligibility(with receiptData: Data, + productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + introEligibilityCalculator + .checkEligibility(with: receiptData, + productIdentifiers: Set(productIdentifiers)) { receivedEligibility, maybeError in + if let error = maybeError { + Logger.error(Strings.receipt.parse_receipt_locally_error(error: error)) + self.fetchIntroEligibilityFromBackend(with: receiptData, + productIdentifiers: productIdentifiers, + completion: completion) + return + } + var convertedEligibility: [String: IntroEligibility] = [:] + for (key, value) in receivedEligibility { + let introEligibility = IntroEligibility(eligibilityStatus: value) + convertedEligibility[key] = introEligibility + } + self.operationDispatcher.dispatchOnMainThread { + completion(convertedEligibility) + } + } + } + + func fetchIntroEligibilityFromBackend(with receiptData: Data, + productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + self.backend.getIntroEligibility(appUserID: self.appUserID, + receiptData: receiptData, + productIdentifiers: productIdentifiers) { backendResult, maybeError in + var result = backendResult + if let error = maybeError { + Logger.error(Strings.purchase.unable_to_get_intro_eligibility_for_user(error: error)) + let resultWithUnknowns = productIdentifiers.reduce(into: [:]) { resultDict, productId in + resultDict[productId] = IntroEligibility(eligibilityStatus: IntroEligibilityStatus.unknown) + } + result = resultWithUnknowns + } + self.operationDispatcher.dispatchOnMainThread { + completion(result) + } + } + } + +} diff --git a/Purchases/Support/BeginRefundRequestHelper.swift b/Purchases/Support/BeginRefundRequestHelper.swift new file mode 100644 index 0000000000..f77441155a --- /dev/null +++ b/Purchases/Support/BeginRefundRequestHelper.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 +// +// BeginRefundRequestHelper.swift +// +// Created by Madeline Beyl on 10/13/21. + +import Foundation + +/** + * Helper class responsible for handling any non-store-specific code involved in beginning a refund request. + * Delegates store-specific operations to `SK2BeginRefundRequestHelper`. + */ +class BeginRefundRequestHelper { + + private let systemInfo: SystemInfo + +#if os(iOS) || targetEnvironment(macCatalyst) + @available(iOS 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + lazy var sk2Helper = SK2BeginRefundRequestHelper() +#endif + + init(systemInfo: SystemInfo) { + self.systemInfo = systemInfo + } + + /* + * Entry point for beginning the refund request. fatalErrors if beginning a refund request is not supported + * on the current platform, else passes the request on to `beginRefundRequest(productID:)`. + */ + @available(iOS 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + func beginRefundRequest(productID: String, completion: @escaping (Result) -> Void) { +#if os(iOS) || targetEnvironment(macCatalyst) + _ = Task { + let result = await self.beginRefundRequest(productID: productID) + completion(result) + } + + return +#else + fatalError(Strings.purchase.begin_refund_request_unsupported.description) +#endif + } + +} + +#if os(iOS) || targetEnvironment(macCatalyst) +@available(iOS 15.0, *) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +private extension BeginRefundRequestHelper { + + /* + * Main worker function for beginning a refund request. Handles getting the current windowScene and verifying the + * transaction before calling into `SK2BeginRefundRequestHelper`'s `initiateRefundRequest`. + */ + @MainActor + @available(iOS 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + func beginRefundRequest(productID: String) async -> Result { + guard let windowScene = systemInfo.sharedUIApplication?.currentWindowScene else { + return .failure(ErrorUtils.storeProblemError(withMessage: "Failed to get UIWindowScene")) + } + + let transactionVerificationResult = await sk2Helper.verifyTransaction(productID: productID) + + switch transactionVerificationResult { + case .failure(let verificationError): + return .failure(verificationError) + case .success(let transactionID): + return await sk2Helper.initiateRefundRequest(transactionID: transactionID, + windowScene: windowScene) + } + } + +} +#endif + +/// Status codes for refund requests. +@objc(RCRefundRequestStatus) public enum RefundRequestStatus: Int { + + /// User canceled submission of the refund request. + @objc(RCRefundRequestUserCancelled) case userCancelled = 0 + /// Apple has received the refund request. + @objc(RCRefundRequestSuccess) case success + /// There was an error with the request. See message for more details. + @objc(RCRefundRequestError) case error + +} diff --git a/RequiresXcode13/Support/ManageSubscriptionModalHelper.swift b/Purchases/Support/ManageSubscriptionsModalHelper.swift similarity index 65% rename from RequiresXcode13/Support/ManageSubscriptionModalHelper.swift rename to Purchases/Support/ManageSubscriptionsModalHelper.swift index 495327218b..1421f026f8 100644 --- a/RequiresXcode13/Support/ManageSubscriptionModalHelper.swift +++ b/Purchases/Support/ManageSubscriptionsModalHelper.swift @@ -13,43 +13,17 @@ import StoreKit -enum ManageSubscriptionsModalError: Error { - - case couldntGetPurchaserInfo(message: String) - case couldntGetWindowScene - case storeKitShowManageSubscriptionsFailed(error: Error) - case couldntGetAppleSubscriptionsURL - -} - -extension ManageSubscriptionsModalError: CustomStringConvertible { - - var description: String { - switch self { - case .couldntGetPurchaserInfo(let message): - return "Failed to get managemementURL from PurchaserInfo. Details: \(message)" - case .couldntGetWindowScene: - return "Failed to get UIWindowScene" - case .storeKitShowManageSubscriptionsFailed(let error): - return "Error when trying to show manage subscription: \(error.localizedDescription)" - case .couldntGetAppleSubscriptionsURL: - return "Error when trying to form the Apple Subscriptions URL." - } - } - -} - class ManageSubscriptionsModalHelper { private let systemInfo: SystemInfo - private let purchaserInfoManager: PurchaserInfoManager + private let customerInfoManager: CustomerInfoManager private let identityManager: IdentityManager init(systemInfo: SystemInfo, - purchaserInfoManager: PurchaserInfoManager, + customerInfoManager: CustomerInfoManager, identityManager: IdentityManager) { self.systemInfo = systemInfo - self.purchaserInfoManager = purchaserInfoManager + self.customerInfoManager = customerInfoManager self.identityManager = identityManager } @@ -57,23 +31,26 @@ class ManageSubscriptionsModalHelper { @available(macOS 10.12, *) @available(watchOS, unavailable) @available(tvOS, unavailable) - func showManageSubscriptionModal(completion: @escaping (Result) -> Void) { + func showManageSubscriptionModal(completion: @escaping (Result) -> Void) { let currentAppUserID = identityManager.currentAppUserID - purchaserInfoManager.purchaserInfo(appUserID: currentAppUserID) { maybePurchaserInfo, maybeError in + customerInfoManager.customerInfo(appUserID: currentAppUserID) { maybeCustomerInfo, maybeError in if let error = maybeError { - completion(.failure(.couldntGetPurchaserInfo(message: error.localizedDescription))) + let message = "Failed to get managemementURL from PurchaserInfo. Details: \(error.localizedDescription)" + completion(.failure(ErrorUtils.customerInfoError(withMessage: message, error: error))) return } - guard let purchaserInfo = maybePurchaserInfo else { - completion(.failure(.couldntGetPurchaserInfo(message: "purchaserInfo is nil."))) + guard let customerInfo = maybeCustomerInfo else { + let message = "Failed to get managemementURL from PurchaserInfo. Details: customerInfo is nil." + completion(.failure(ErrorUtils.customerInfoError(withMessage: message))) return } - guard let managementURL = purchaserInfo.managementURL else { + guard let managementURL = customerInfo.managementURL else { Logger.debug(Strings.purchase.management_url_nil_opening_default) guard let appleSubscriptionsURL = self.systemInfo.appleSubscriptionsURL else { - completion(.failure(.couldntGetAppleSubscriptionsURL)) + let message = "Error when trying to form the Apple Subscriptions URL." + completion(.failure(ErrorUtils.systemInfoError(withMessage: message))) return } self.showAppleManageSubscriptions(managementURL: appleSubscriptionsURL, completion: completion) @@ -98,10 +75,10 @@ class ManageSubscriptionsModalHelper { private extension ManageSubscriptionsModalHelper { func showAppleManageSubscriptions(managementURL: URL, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { #if os(iOS) if #available(iOS 15.0, *) { - Task.init { + _ = Task { let result = await self.showSK2ManageSubscriptions() completion(result) } @@ -111,7 +88,7 @@ private extension ManageSubscriptionsModalHelper { openURL(managementURL, completion: completion) } - func openURL(_ url: URL, completion: @escaping (Result) -> Void) { + func openURL(_ url: URL, completion: @escaping (Result) -> Void) { #if os(iOS) openURLIfNotAppExtension(url: url) #elseif os(macOS) @@ -145,24 +122,23 @@ private extension ManageSubscriptionsModalHelper { @MainActor @available(iOS 15.0, *) @available(macOS, unavailable) - func showSK2ManageSubscriptions() async -> Result { + func showSK2ManageSubscriptions() async -> Result { guard let application = systemInfo.sharedUIApplication, let windowScene = application.currentWindowScene else { - return .failure(.couldntGetWindowScene) + return .failure(ErrorUtils.storeProblemError(withMessage: "Failed to get UIWindowScene")) } do { -// todo: remove when this gets fixed. -// limiting to arm architecture since builds on beta 5 fail if other archs are included -#if os(iOS) && arch(arm64) +#if os(iOS) try await AppStore.showManageSubscriptions(in: windowScene) return .success(()) #else fatalError("tried to call AppStore.showManageSubscriptions in a platform that doesn't support it!") #endif - } catch let error { - return .failure(.storeKitShowManageSubscriptionsFailed(error: error)) + } catch { + let message = "Error when trying to show manage subscription: \(error.localizedDescription)" + return .failure(ErrorUtils.storeProblemError(withMessage: message, error: error)) } } #endif diff --git a/PurchasesTests/Caching/DeviceCacheTests.swift b/PurchasesTests/Caching/DeviceCacheTests.swift index 7f90a0d008..452f28e894 100644 --- a/PurchasesTests/Caching/DeviceCacheTests.swift +++ b/PurchasesTests/Caching/DeviceCacheTests.swift @@ -179,12 +179,12 @@ class DeviceCacheTests: XCTestCase { func testOfferingsAreProperlyCached() { let products = [ - "com.myproduct.annual": MockSKProduct(mockProductIdentifier: "com.myproduct.annual"), - "com.myproduct.monthly": MockSKProduct(mockProductIdentifier: "com.myproduct.monthly") + "com.myproduct.annual": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.annual")), + "com.myproduct.monthly": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.monthly")) ] let offeringIdentifier = "offering_a" let serverDescription = "This is the base offering" - let optionalOffering = OfferingsFactory().createOffering(withProducts: products, offeringData: [ + let optionalOffering = OfferingsFactory().createOffering(fromProductDetailsByID: products, offeringData: [ "identifier": offeringIdentifier, "description": serverDescription, "packages": [ diff --git a/PurchasesTests/FoundationExtensions/DictionaryExtensionsTests.swift b/PurchasesTests/FoundationExtensions/DictionaryExtensionsTests.swift index 1879dc8f64..8067b8047a 100644 --- a/PurchasesTests/FoundationExtensions/DictionaryExtensionsTests.swift +++ b/PurchasesTests/FoundationExtensions/DictionaryExtensionsTests.swift @@ -87,4 +87,15 @@ class DictionaryExtensionsTests: XCTestCase { expect(obtainedDict).to(equal(expectedDict)) } + func testAddEntriesToDictionaryWithOperatorPlusAddsValuesCorrectly() { + var original = ["a": "1", "b": "1"] + let addedValues = ["a": "2", "b": "2", "c": "2"] + let expectedDict = ["a": "2", "b": "2", "c": "2"] + + original += addedValues + + expect(original.keys.count).to(equal(expectedDict.keys.count)) + expect(original).to(equal(expectedDict)) + } + } diff --git a/PurchasesTests/Identity/IdentityManagerTests.swift b/PurchasesTests/Identity/IdentityManagerTests.swift index b3bc1e382e..f516564d23 100644 --- a/PurchasesTests/Identity/IdentityManagerTests.swift +++ b/PurchasesTests/Identity/IdentityManagerTests.swift @@ -14,11 +14,11 @@ class IdentityManagerTests: XCTestCase { private var mockDeviceCache: MockDeviceCache! private let mockBackend = MockBackend() private let mockCustomerInfoManager = MockCustomerInfoManager(operationDispatcher: MockOperationDispatcher(), - deviceCache: MockDeviceCache(), - backend: MockBackend(), - systemInfo: try! MockSystemInfo(platformFlavor: nil, - platformFlavorVersion: nil, - finishTransactions: false)) + deviceCache: MockDeviceCache(), + backend: MockBackend(), + systemInfo: try! MockSystemInfo(platformFlavor: nil, + platformFlavorVersion: nil, + finishTransactions: false)) let mockCustomerInfo = CustomerInfo(testData: [ "request_date": "2019-08-16T10:30:42Z", diff --git a/PurchasesTests/Mocks/MockBackend.swift b/PurchasesTests/Mocks/MockBackend.swift index c1214b2d4d..456a85923a 100644 --- a/PurchasesTests/Mocks/MockBackend.swift +++ b/PurchasesTests/Mocks/MockBackend.swift @@ -89,6 +89,7 @@ class MockBackend: Backend { receiptData: Data?, productIdentifiers: [String]?, completion: IntroEligibilityResponseHandler?)]() + var stubbedGetIntroEligibilityCompletionResult: (eligibilities: [String: IntroEligibility], error: Error?)? override func getIntroEligibility(appUserID: String, receiptData: Data, @@ -98,6 +99,7 @@ class MockBackend: Backend { invokedGetIntroEligibilityCount += 1 invokedGetIntroEligibilityParameters = (appUserID, receiptData, productIdentifiers, completion) invokedGetIntroEligibilityParametersList.append((appUserID, receiptData, productIdentifiers, completion)) + completion(stubbedGetIntroEligibilityCompletionResult?.eligibilities ?? [:], stubbedGetIntroEligibilityCompletionResult?.error) } var invokedGetOfferingsForAppUserID = false diff --git a/PurchasesTests/Mocks/MockBeginRefundRequestHelper.swift b/PurchasesTests/Mocks/MockBeginRefundRequestHelper.swift new file mode 100644 index 0000000000..7ec0fa0ab2 --- /dev/null +++ b/PurchasesTests/Mocks/MockBeginRefundRequestHelper.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 +// +// MockBeginRefundRequestHelper.swift +// +// Created by Madeline Beyl on 10/15/21. + +import Foundation +@testable import RevenueCat + +class MockBeginRefundRequestHelper: BeginRefundRequestHelper { + + var maybeMockError: Error? + var maybeMockRefundRequestStatus: RefundRequestStatus? + +#if os(iOS) || targetEnvironment(macCatalyst) + @available(iOS 15.0, macCatalyst 15, *) + @available(watchOS, unavailable) + @available(macOS, unavailable) + @available(tvOS, unavailable) + override func beginRefundRequest(productID: String, + completion: @escaping (Result) -> Void) { + if let error = maybeMockError { + completion(.failure(error)) + } else { + completion(.success(maybeMockRefundRequestStatus ?? RefundRequestStatus.success)) + } + } +#endif + +} diff --git a/PurchasesTests/Mocks/MockBundle.swift b/PurchasesTests/Mocks/MockBundle.swift index 967d053384..552afa08b3 100644 --- a/PurchasesTests/Mocks/MockBundle.swift +++ b/PurchasesTests/Mocks/MockBundle.swift @@ -34,7 +34,7 @@ class MockBundle: Bundle { return testBundle .url(forResource: Self.mockAppStoreReceiptFileName, withExtension: "txt") case .emptyReceipt: - return Bundle.main.appStoreReceiptURL + return URL(string: "") case .sandboxReceipt: return testBundle .url(forResource: Self.mockSandboxReceiptFileName, withExtension: "txt") diff --git a/PurchasesTests/Mocks/MockIntroEligibilityCalculator.swift b/PurchasesTests/Mocks/MockIntroEligibilityCalculator.swift index d1ffbf8719..ab326ee0fa 100644 --- a/PurchasesTests/Mocks/MockIntroEligibilityCalculator.swift +++ b/PurchasesTests/Mocks/MockIntroEligibilityCalculator.swift @@ -11,23 +11,23 @@ import Foundation @testable import RevenueCat class MockIntroEligibilityCalculator: IntroEligibilityCalculator { - + var invokedCheckTrialOrIntroductoryPriceEligibility = false var invokedCheckTrialOrIntroductoryPriceEligibilityCount = 0 var invokedCheckTrialOrIntroductoryPriceEligibilityParameters: (receiptData: Data, candidateProductIdentifiers: Set)? var invokedCheckTrialOrIntroductoryPriceEligibilityParametersList = [(receiptData: Data, - candidateProductIdentifiers: Set)]() - var stubbedCheckTrialOrIntroductoryPriceEligibilityCompletionResult: ([String: NSNumber], Error?)? - + candidateProductIdentifiers: Set)]() + var stubbedCheckTrialOrIntroductoryPriceEligibilityResult: ([String: IntroEligibilityStatus], Error?)? + @available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *) - override func checkTrialOrIntroductoryPriceEligibility(with receiptData: Data, - productIdentifiers candidateProductIdentifiers: Set, - completion: @escaping ([String: NSNumber], Error?) -> ()) { + override func checkEligibility(with receiptData: Data, + productIdentifiers candidateProductIdentifiers: Set, + completion: @escaping ([String: IntroEligibilityStatus], Error?) -> ()) { invokedCheckTrialOrIntroductoryPriceEligibility = true invokedCheckTrialOrIntroductoryPriceEligibilityCount += 1 invokedCheckTrialOrIntroductoryPriceEligibilityParameters = (receiptData, candidateProductIdentifiers) invokedCheckTrialOrIntroductoryPriceEligibilityParametersList.append((receiptData, candidateProductIdentifiers)) - if let result = stubbedCheckTrialOrIntroductoryPriceEligibilityCompletionResult { + if let result = stubbedCheckTrialOrIntroductoryPriceEligibilityResult { completion(result.0, result.1) } } diff --git a/PurchasesTests/Mocks/MockManageSubscriptionsModalHelper.swift b/PurchasesTests/Mocks/MockManageSubscriptionsModalHelper.swift new file mode 100644 index 0000000000..41e3608503 --- /dev/null +++ b/PurchasesTests/Mocks/MockManageSubscriptionsModalHelper.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 +// +// MockManageSubsModalHelper.swift +// +// Created by César de la Vega on 10/8/21. + +import Foundation +@testable import RevenueCat + +class MockManageSubscriptionsModalHelper: ManageSubscriptionsModalHelper { + + var mockError: Error? + +#if os(iOS) || os(macOS) + @available(iOS 9.0, *) + @available(macOS 10.12, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + override func showManageSubscriptionModal(completion: @escaping (Result) -> Void) { + if let error = mockError { + completion(.failure(error)) + } else { + completion(.success(Void())) + } + } +#endif + +} diff --git a/PurchasesTests/Mocks/MockNotificationCenter.swift b/PurchasesTests/Mocks/MockNotificationCenter.swift index 0016c849cc..20c8b49732 100644 --- a/PurchasesTests/Mocks/MockNotificationCenter.swift +++ b/PurchasesTests/Mocks/MockNotificationCenter.swift @@ -2,7 +2,6 @@ // Created by RevenueCat on 3/2/20. // Copyright (c) 2020 Purchases. All rights reserved. // - import Foundation class MockNotificationCenter: NotificationCenter { diff --git a/PurchasesTests/Mocks/MockOfferingsFactory.swift b/PurchasesTests/Mocks/MockOfferingsFactory.swift index f115045246..74fafda503 100644 --- a/PurchasesTests/Mocks/MockOfferingsFactory.swift +++ b/PurchasesTests/Mocks/MockOfferingsFactory.swift @@ -10,7 +10,7 @@ class MockOfferingsFactory: OfferingsFactory { var emptyOfferings = false var nilOfferings = false - override func createOfferings(withProducts products: [String: SKProduct], + override func createOfferings(fromProductDetailsByID products: [String: ProductDetails], data: [String: Any]) -> Offerings? { if (emptyOfferings) { return Offerings(offerings: [:], currentOfferingID: "base") @@ -18,6 +18,9 @@ class MockOfferingsFactory: OfferingsFactory { if (nilOfferings) { return nil } + + let product = MockSK1Product(mockProductIdentifier: "monthly_freetrial") + let productDetails = SK1ProductDetails(sk1Product: product) return Offerings( offerings: [ "base": Offering( @@ -26,7 +29,7 @@ class MockOfferingsFactory: OfferingsFactory { availablePackages: [ Package(identifier: "$rc_monthly", packageType: PackageType.monthly, - product: MockSKProduct(mockProductIdentifier: "monthly_freetrial"), + productDetails: productDetails, offeringIdentifier: "base") ] )], diff --git a/PurchasesTests/Mocks/MockProductsManager.swift b/PurchasesTests/Mocks/MockProductsManager.swift index cc3529d7ab..5baf351242 100644 --- a/PurchasesTests/Mocks/MockProductsManager.swift +++ b/PurchasesTests/Mocks/MockProductsManager.swift @@ -9,13 +9,73 @@ import StoreKit class MockProductsManager: ProductsManager { + var invokedProductsFromOptimalStoreKitVersionWithIdentifiers = false + var invokedProductsFromOptimalStoreKitVersionWithIdentifiersCount = 0 + var invokedProductsFromOptimalStoreKitVersionWithIdentifiersParameters: (identifiers: Set, Void)? + var invokedProductsFromOptimalStoreKitVersionWithIdentifiersParametersList = [(identifiers: Set, Void)]() + var stubbedProductsFromOptimalStoreKitVersionWithIdentifiersCompletionResult: (Set, Void)? + + override func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: Set, + completion: @escaping (Set) -> Void) { + invokedProductsFromOptimalStoreKitVersionWithIdentifiers = true + invokedProductsFromOptimalStoreKitVersionWithIdentifiersCount += 1 + invokedProductsFromOptimalStoreKitVersionWithIdentifiersParameters = (identifiers, ()) + invokedProductsFromOptimalStoreKitVersionWithIdentifiersParametersList.append((identifiers, ())) + if let result = stubbedProductsFromOptimalStoreKitVersionWithIdentifiersCompletionResult { + completion(result.0) + } else { + let products: [SK1Product] = identifiers.map { (identifier) -> MockSK1Product in + let p = MockSK1Product(mockProductIdentifier: identifier) + p.mockSubscriptionGroupIdentifier = "1234567" + if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { + let mockDiscount = MockDiscount() + mockDiscount.mockIdentifier = "discount_id" + p.mockDiscount = mockDiscount + } + return p + } + let result = Set(products).map { SK1ProductDetails(sk1Product: $0) } + + completion(Set(result)) + } + } + + var invokedProductsFromOptimalStoreKitVersion = false + var invokedProductsFromOptimalStoreKitVersionCount = 0 + var invokedProductsFromOptimalStoreKitVersionParameters: (identifiers: Set, Void)? + var invokedProductsFromOptimalStoreKitVersionParametersList = [(identifiers: Set, Void)]() + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + override func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: Set) async -> Set { + invokedProductsFromOptimalStoreKitVersion = true + invokedProductsFromOptimalStoreKitVersionCount += 1 + invokedProductsFromOptimalStoreKitVersionParameters = (identifiers, ()) + invokedProductsFromOptimalStoreKitVersionParametersList.append((identifiers, ())) + let result = stubbedProductsFromOptimalStoreKitVersionWithIdentifiersCompletionResult?.0 ?? Set() + return result + } + + var invokedSk2ProductDetails = false + var invokedSk2ProductDetailsCount = 0 + var invokedSk2ProductDetailsParameters: (identifiers: Set, Void)? + var invokedSk2ProductDetailsParametersList = [(identifiers: Set, Void)]() + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + override func sk2ProductDetails(withIdentifiers identifiers: Set) async -> Set { + invokedSk2ProductDetails = true + invokedSk2ProductDetailsCount += 1 + invokedSk2ProductDetailsParameters = (identifiers, ()) + invokedSk2ProductDetailsParametersList.append((identifiers, ())) + return Set() + } + var invokedProducts = false var invokedProductsCount = 0 var invokedProductsParameters: Set? var invokedProductsParametersList = [Set]() - var stubbedProductsCompletionResult: Set? + var stubbedProductsCompletionResult: Set? - override func products(withIdentifiers identifiers: Set, completion: @escaping (Set) -> Void) { + override func products(withIdentifiers identifiers: Set, completion: @escaping (Set) -> Void) { invokedProducts = true invokedProductsCount += 1 invokedProductsParameters = identifiers @@ -23,8 +83,8 @@ class MockProductsManager: ProductsManager { if let result = stubbedProductsCompletionResult { completion(result) } else { - let products: [SKProduct] = identifiers.map { (identifier) -> MockSKProduct in - let p = MockSKProduct(mockProductIdentifier: identifier) + let products: [SK1Product] = identifiers.map { (identifier) -> MockSK1Product in + let p = MockSK1Product(mockProductIdentifier: identifier) p.mockSubscriptionGroupIdentifier = "1234567" if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { let mockDiscount = MockDiscount() @@ -39,9 +99,9 @@ class MockProductsManager: ProductsManager { var invokedCacheProduct = false var invokedCacheProductCount = 0 - var invokedCacheProductParameter: SKProduct? + var invokedCacheProductParameter: SK1Product? - override func cacheProduct(_ product: SKProduct) { + override func cacheProduct(_ product: SK1Product) { invokedCacheProduct = true invokedCacheProductCount += 1 invokedCacheProductParameter = product diff --git a/PurchasesTests/Mocks/MockProductsRequest.swift b/PurchasesTests/Mocks/MockProductsRequest.swift index 774c523a8a..4a9d16ac89 100644 --- a/PurchasesTests/Mocks/MockProductsRequest.swift +++ b/PurchasesTests/Mocks/MockProductsRequest.swift @@ -8,16 +8,16 @@ import StoreKit @testable import RevenueCat class MockProductResponse: SKProductsResponse { - var mockProducts: [MockSKProduct] + var mockProducts: [MockSK1Product] init(productIdentifiers: Set) { self.mockProducts = productIdentifiers.map { identifier in - return MockSKProduct(mockProductIdentifier: identifier) + return MockSK1Product(mockProductIdentifier: identifier) } super.init() } - override var products: [SKProduct] { + override var products: [SK1Product] { return self.mockProducts } } diff --git a/PurchasesTests/Mocks/MockPurchasesDelegate.swift b/PurchasesTests/Mocks/MockPurchasesDelegate.swift index cd1f802b53..8485a94af1 100644 --- a/PurchasesTests/Mocks/MockPurchasesDelegate.swift +++ b/PurchasesTests/Mocks/MockPurchasesDelegate.swift @@ -17,12 +17,12 @@ public class MockPurchasesDelegate: NSObject, PurchasesDelegate { self.customerInfo = customerInfo } - var promoProduct: SKProduct? + var promoProduct: SK1Product? var makeDeferredPurchase: DeferredPromotionalPurchaseBlock? public func purchases(_ purchases: Purchases, - shouldPurchasePromoProduct product: SKProduct, + shouldPurchasePromoProduct product: SK1Product, defermentBlock makeDeferredPurchase: @escaping (@escaping PurchaseCompletedBlock) -> Void) { promoProduct = product self.makeDeferredPurchase = makeDeferredPurchase diff --git a/PurchasesTests/Mocks/MockSKProduct.swift b/PurchasesTests/Mocks/MockSK1Product.swift similarity index 90% rename from PurchasesTests/Mocks/MockSKProduct.swift rename to PurchasesTests/Mocks/MockSK1Product.swift index c96d54ae60..5f4214ee8f 100644 --- a/PurchasesTests/Mocks/MockSKProduct.swift +++ b/PurchasesTests/Mocks/MockSK1Product.swift @@ -3,8 +3,9 @@ // Copyright (c) 2020 Purchases. All rights reserved. // import StoreKit +@testable import RevenueCat -class MockSKProduct: SKProduct { +class MockSK1Product: SK1Product { var mockProductIdentifier: String init(mockProductIdentifier: String, mockSubscriptionGroupIdentifier: String? = nil) { @@ -46,10 +47,10 @@ class MockSKProduct: SKProduct { } @available(iOS 11.2, macCatalyst 13.0, tvOS 11.2, macOS 10.13.2, *) - lazy var mockSubscriptionPeriod: SKProductSubscriptionPeriod? = nil + lazy var mockSubscriptionPeriod: SKProductSubscriptionPeriod? = SKProductSubscriptionPeriod(numberOfUnits: 1, unit: .month) @available(iOS 11.2, macCatalyst 13.0, tvOS 11.2, macOS 10.13.2, *) - override var subscriptionPeriod: SKProductSubscriptionPeriod { - return mockSubscriptionPeriod ?? SKProductSubscriptionPeriod(numberOfUnits: 1, unit:.month) + override var subscriptionPeriod: SKProductSubscriptionPeriod? { + return mockSubscriptionPeriod } } diff --git a/PurchasesTests/Mocks/MockSK2BeginRefundRequestHelper.swift b/PurchasesTests/Mocks/MockSK2BeginRefundRequestHelper.swift new file mode 100644 index 0000000000..abaed083f6 --- /dev/null +++ b/PurchasesTests/Mocks/MockSK2BeginRefundRequestHelper.swift @@ -0,0 +1,64 @@ +// +// 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 +// +// MockRefundRequestHelperSK2.swift +// +// Created by Madeline Beyl on 10/21/21. + +import Foundation +@testable import RevenueCat +import StoreKit + +@available(iOS 15.0, macCatalyst 15.0, *) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +@available(tvOS, unavailable) +class MockSK2BeginRefundRequestHelper: SK2BeginRefundRequestHelper { + + var maybeMockSK2Error: Error? + + // We can't directly store instances of StoreKit.Transaction.RefundRequestStatus, since that causes + // linking issues in iOS < 15, even with @available checks correctly in place. + // So instead, we store the underlying product as Any and wrap it with casting. + // https://openradar.appspot.com/radar?id=4970535809187840 + private var untypedSK2Status: Any? + var maybeMockSK2Status: StoreKit.Transaction.RefundRequestStatus? { + get { + return untypedSK2Status as! StoreKit.Transaction.RefundRequestStatus? + } + set { + untypedSK2Status = newValue + } + } + + var transactionVerified = true + var refundRequestCalled = false + var verifyTransactionCalled = false + + override func initiateSK2RefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async -> + Result { + refundRequestCalled = true + if let error = maybeMockSK2Error { + return .failure(error) + } else { + return .success(maybeMockSK2Status ?? StoreKit.Transaction.RefundRequestStatus.success) + } + } + + override func verifyTransaction(productID: String) async -> Result { + verifyTransactionCalled = true + if transactionVerified { + return .success(UInt64()) + } else { + let message = "Test error" + return .failure(ErrorUtils.beginRefundRequestError(withMessage: message)) + } + } + +} diff --git a/PurchasesTests/Mocks/MockStoreKit2TransactionListener.swift b/PurchasesTests/Mocks/MockStoreKit2TransactionListener.swift new file mode 100644 index 0000000000..0fc0005142 --- /dev/null +++ b/PurchasesTests/Mocks/MockStoreKit2TransactionListener.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 +// +// MockStoreKit2TransactionListener.swift +// +// Created by Andrés Boedo on 23/9/21. + +import Foundation +@testable import RevenueCat +import StoreKit + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 7.0, *) +class MockStoreKit2TransactionListener: StoreKit2TransactionListener { + + convenience init() { + self.init(delegate: nil) + } + + var invokedDelegateSetter = false + var invokedDelegateSetterCount = 0 + var invokedDelegate: StoreKit2TransactionListenerDelegate? + var invokedDelegateList = [StoreKit2TransactionListenerDelegate?]() + var invokedDelegateGetter = false + var invokedDelegateGetterCount = 0 + var stubbedDelegate: StoreKit2TransactionListenerDelegate! + + override var delegate: StoreKit2TransactionListenerDelegate? { + set { + invokedDelegateSetter = true + invokedDelegateSetterCount += 1 + invokedDelegate = newValue + invokedDelegateList.append(newValue) + } + get { + invokedDelegateGetter = true + invokedDelegateGetterCount += 1 + return stubbedDelegate + } + } + + var invokedListenForTransactions = false + var invokedListenForTransactionsCount = 0 + + override func listenForTransactions() { + invokedListenForTransactions = true + invokedListenForTransactionsCount += 1 + } + + var invokedHandle = false + var invokedHandleCount = 0 + // purchaseResult will always be an instance of StoreKit.Product.PurchaseResult + // but we can't store it directly as a property. + // see https://openradar.appspot.com/radar?id=4970535809187840 + var invokedHandleParameters: (purchaseResult: Any, Void)? + var invokedHandleParametersList = [(purchaseResult: Any, Void)]() + + override func handle(purchaseResult: StoreKit.Product.PurchaseResult) async -> Bool { + invokedHandle = true + invokedHandleCount += 1 + invokedHandleParameters = (purchaseResult, ()) + invokedHandleParametersList.append((purchaseResult, ())) + return false + } +} diff --git a/PurchasesTests/Mocks/MockTrialOrIntroPriceEligibilityChecker.swift b/PurchasesTests/Mocks/MockTrialOrIntroPriceEligibilityChecker.swift new file mode 100644 index 0000000000..abf3bdb3df --- /dev/null +++ b/PurchasesTests/Mocks/MockTrialOrIntroPriceEligibilityChecker.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 +// +// MockTrialOrIntroPriceEligibilityChecker.swift +// +// Created by César de la Vega on 9/1/21. + +@testable import RevenueCat + +class MockTrialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityChecker { + + convenience init() { + let systemInfo = try! MockSystemInfo(platformFlavor: "xyz", platformFlavorVersion: "123", finishTransactions: true) + let productsManager = MockProductsManager(systemInfo: systemInfo) + self.init(receiptFetcher: MockReceiptFetcher(requestFetcher: MockRequestFetcher(), systemInfo: systemInfo), + introEligibilityCalculator: MockIntroEligibilityCalculator(productsManager: productsManager, + receiptParser: MockReceiptParser()), + backend: MockBackend(), + identityManager: MockIdentityManager(mockAppUserID: "app_user"), + operationDispatcher: MockOperationDispatcher(), + productsManager: MockProductsManager(systemInfo: systemInfo)) + } + + var invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore = false + var invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreCount = 0 + var invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters: (productIdentifiers: [String], Void)? + var invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParametersList = [(productIdentifiers: [String], Void)]() + var stubbedCheckTrialOrIntroPriceEligibilityFromOptimalStoreReceiveEligibilityResult: [String: IntroEligibility] = [:] + + override func checkEligibility(productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore = true + invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreCount += 1 + invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters = (productIdentifiers, ()) + invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParametersList.append((productIdentifiers, ())) + completion(stubbedCheckTrialOrIntroPriceEligibilityFromOptimalStoreReceiveEligibilityResult) + } + + var invokedSk1checkTrialOrIntroPriceEligibility = false + var invokedSk1checkTrialOrIntroPriceEligibilityCount = 0 + var invokedSk1checkTrialOrIntroPriceEligibilityParameters: (productIdentifiers: [String], Void)? + var invokedSk1checkTrialOrIntroPriceEligibilityParametersList = [(productIdentifiers: [String], Void)]() + var stubbedSk1checkTrialOrIntroPriceEligibilityReceiveEligibilityResult: [String: IntroEligibility] = [:] + + override func sk1CheckEligibility(_ productIdentifiers: [String], + completion: @escaping ReceiveIntroEligibilityBlock) { + invokedSk1checkTrialOrIntroPriceEligibility = true + invokedSk1checkTrialOrIntroPriceEligibilityCount += 1 + invokedSk1checkTrialOrIntroPriceEligibilityParameters = (productIdentifiers, ()) + invokedSk1checkTrialOrIntroPriceEligibilityParametersList.append((productIdentifiers, ())) + completion(stubbedSk1checkTrialOrIntroPriceEligibilityReceiveEligibilityResult) + } + + var invokedSk2checkTrialOrIntroPriceEligibility = false + var invokedSk2checkTrialOrIntroPriceEligibilityCount = 0 + var invokedSk2checkTrialOrIntroPriceEligibilityParameters: (productIdentifiers: [String], Void)? + var invokedSk2checkTrialOrIntroPriceEligibilityParametersList = [(productIdentifiers: [String], Void)]() + var stubbedSk2checkTrialOrIntroPriceEligibilityReceiveEligibilityResult: [String: IntroEligibility] = [:] + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + override func sk2CheckEligibility(_ productIdentifiers: [String]) async -> [String : IntroEligibility] { + invokedSk2checkTrialOrIntroPriceEligibility = true + invokedSk2checkTrialOrIntroPriceEligibilityCount += 1 + invokedSk2checkTrialOrIntroPriceEligibilityParameters = (productIdentifiers, ()) + invokedSk2checkTrialOrIntroPriceEligibilityParametersList.append((productIdentifiers, ())) + + return stubbedSk2checkTrialOrIntroPriceEligibilityReceiveEligibilityResult + } +} diff --git a/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift b/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift index 4ba2cb8b3d..5604d49239 100644 --- a/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift +++ b/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift @@ -8,11 +8,12 @@ import Foundation import StoreKit +import RevenueCat @available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *) extension SKProductSubscriptionPeriod { convenience init(numberOfUnits: Int, - unit: SKProduct.PeriodUnit) { + unit: SK1Product.PeriodUnit) { self.init() self.setValue(numberOfUnits, forKey: "numberOfUnits") self.setValue(unit.rawValue, forKey: "unit") diff --git a/PurchasesTests/Networking/HTTPClientTests.swift b/PurchasesTests/Networking/HTTPClientTests.swift index 7bafa73ece..2931a5ebeb 100644 --- a/PurchasesTests/Networking/HTTPClientTests.swift +++ b/PurchasesTests/Networking/HTTPClientTests.swift @@ -40,12 +40,12 @@ class HTTPClientTests: XCTestCase { guard let host = SystemInfo.serverHostURL.host else { fatalError() } stub(condition: isHost(host)) { _ in hostCorrect = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, path: path, - requestBody: Dictionary.init(), + requestBody: [:], headers: [:], completionHandler: nil) @@ -58,7 +58,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("test_header")) { _ in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -76,7 +76,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("content-type", value: "application/json")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -94,7 +94,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Platform", value: SystemInfo.platformHeader)) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -112,7 +112,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Version", value: Purchases.frameworkVersion)) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -130,7 +130,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Platform-Version", value: ProcessInfo().operatingSystemVersionString)) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -148,7 +148,7 @@ class HTTPClientTests: XCTestCase { stub(condition: isPath("/v1" + path)) { _ in pathHit = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -169,7 +169,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasBody(bodyData)) { _ in pathHit = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -186,7 +186,7 @@ class HTTPClientTests: XCTestCase { var completionCalled = false stub(condition: isPath("/v1" + path)) { _ in - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performGETRequest(serially: true, @@ -204,7 +204,7 @@ class HTTPClientTests: XCTestCase { let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil) stub(condition: isPath("/v1" + path)) { request in - let response = HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + let response = HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) response.error = error return response } @@ -323,7 +323,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Client-Version", value: version )) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -343,7 +343,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Client-Build-Version", value: version )) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -364,7 +364,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Apple-Device-Identifier", value: idfv )) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -383,7 +383,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Platform-Flavor", value: "native")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.client.performPOSTRequest(serially: true, @@ -401,7 +401,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Platform-Flavor", value: "react-native")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } let systemInfo = try SystemInfo(platformFlavor: "react-native", platformFlavorVersion: "3.2.1", @@ -422,7 +422,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Platform-Flavor-Version", value: "1.2.3")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } let systemInfo = try SystemInfo(platformFlavor: "react-native", platformFlavorVersion: "1.2.3", @@ -444,7 +444,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Observer-Mode-Enabled", value: "false")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } let systemInfo = try SystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) let client = HTTPClient(systemInfo: systemInfo, eTagManager: eTagManager) @@ -463,7 +463,7 @@ class HTTPClientTests: XCTestCase { stub(condition: hasHeaderNamed("X-Observer-Mode-Enabled", value: "true")) { request in headerPresent = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } let systemInfo = try SystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: false) let client = HTTPClient(systemInfo: systemInfo, eTagManager: eTagManager) @@ -772,7 +772,7 @@ class HTTPClientTests: XCTestCase { self.eTagManager.shouldReturnResultFromBackend = true } firstTimeCalled = true - return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) } self.eTagManager.shouldReturnResultFromBackend = false diff --git a/PurchasesTests/Purchasing/ErrorCodeTests.swift b/PurchasesTests/Purchasing/ErrorCodeTests.swift index abc6a4894c..f9f3996596 100644 --- a/PurchasesTests/Purchasing/ErrorCodeTests.swift +++ b/PurchasesTests/Purchasing/ErrorCodeTests.swift @@ -138,8 +138,23 @@ class ErrorCodeTests: XCTestCase { expectedRawValue: 28) } + func testCustomerInfoError() { + ensureEnumCaseMatchesExpectedRawValue(errorCode: .customerInfoError, + expectedRawValue: 29) + } + + func testSystemInfoError() { + ensureEnumCaseMatchesExpectedRawValue(errorCode: .systemInfoError, + expectedRawValue: 30) + } + + func testBeginRefundRequestError() { + ensureEnumCaseMatchesExpectedRawValue(errorCode: .beginRefundRequestError, + expectedRawValue: 31) + } + func testErrorCodeEnumCasesAreCoveredInTests() { - expect(ErrorCode.allCases.count).to(equal(29)) + expect(ErrorCode.allCases.count).to(equal(32)) } func ensureEnumCaseMatchesExpectedRawValue(errorCode: ErrorCode, expectedRawValue: Int) { diff --git a/PurchasesTests/Purchasing/IntroEligibilityCalculatorTests.swift b/PurchasesTests/Purchasing/IntroEligibilityCalculatorTests.swift index 0921112cda..f5dcf97d26 100644 --- a/PurchasesTests/Purchasing/IntroEligibilityCalculatorTests.swift +++ b/PurchasesTests/Purchasing/IntroEligibilityCalculatorTests.swift @@ -7,22 +7,24 @@ import Nimble class IntroEligibilityCalculatorTests: XCTestCase { var calculator: IntroEligibilityCalculator! - let mockProductsManager = MockProductsManager() + var systemInfo: MockSystemInfo! + var mockProductsManager: MockProductsManager! let mockReceiptParser = MockReceiptParser() - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() + systemInfo = try MockSystemInfo(platformFlavor: "xyz", platformFlavorVersion: "123", finishTransactions: true) + self.mockProductsManager = MockProductsManager(systemInfo: systemInfo) calculator = IntroEligibilityCalculator(productsManager: mockProductsManager, receiptParser: mockReceiptParser) } func testCheckTrialOrIntroductoryPriceEligibilityReturnsEmptyIfNoProductIds() { var receivedError: Error? = nil - var receivedEligibility: [String: NSNumber]? = nil + var receivedEligibility: [String: IntroEligibilityStatus]? = nil var completionCalled = false - calculator.checkTrialOrIntroductoryPriceEligibility(with: Data(), - productIdentifiers: Set()) { eligibilityByProductId, - error in + calculator.checkEligibility(with: Data(), + productIdentifiers: Set()) { eligibilityByProductId, error in completionCalled = true receivedError = error receivedEligibility = eligibilityByProductId @@ -36,14 +38,14 @@ class IntroEligibilityCalculatorTests: XCTestCase { func testCheckTrialOrIntroductoryPriceEligibilityReturnsErrorIfReceiptParserThrows() { var receivedError: Error? = nil - var receivedEligibility: [String: NSNumber]? = nil + var receivedEligibility: [String: IntroEligibilityStatus]? = nil var completionCalled = false let productIdentifiers = Set(["com.revenuecat.test"]) mockReceiptParser.stubbedParseError = ReceiptReadingError.receiptParsingError - calculator.checkTrialOrIntroductoryPriceEligibility(with: Data(), - productIdentifiers: productIdentifiers) { + calculator.checkEligibility(with: Data(), + productIdentifiers: productIdentifiers) { eligibilityByProductId, error in completionCalled = true @@ -65,12 +67,12 @@ class IntroEligibilityCalculatorTests: XCTestCase { let receiptIdentifiers = receipt.purchasedIntroOfferOrFreeTrialProductIdentifiers() mockProductsManager.stubbedProductsCompletionResult = Set(["a", "b"].map { - MockSKProduct(mockProductIdentifier: $0) + MockSK1Product(mockProductIdentifier: $0) }) let candidateIdentifiers = Set(["a", "b", "c"]) - calculator.checkTrialOrIntroductoryPriceEligibility(with: Data(), - productIdentifiers: Set(candidateIdentifiers)) { _, _ in + calculator.checkEligibility(with: Data(), + productIdentifiers: Set(candidateIdentifiers)) { _, _ in completionCalled = true } @@ -81,16 +83,16 @@ class IntroEligibilityCalculatorTests: XCTestCase { func testCheckTrialOrIntroductoryPriceEligibilityGetsCorrectResult() { var receivedError: Error? = nil - var receivedEligibility: [String: NSNumber]? = nil + var receivedEligibility: [String: IntroEligibilityStatus]? = nil var completionCalled = false let receipt = mockReceipt() mockReceiptParser.stubbedParseResult = receipt - let product1 = MockSKProduct(mockProductIdentifier: "com.revenuecat.product1", + let product1 = MockSK1Product(mockProductIdentifier: "com.revenuecat.product1", mockSubscriptionGroupIdentifier: "group1") product1.mockDiscount = MockDiscount() - let product2 = MockSKProduct(mockProductIdentifier: "com.revenuecat.product2", + let product2 = MockSK1Product(mockProductIdentifier: "com.revenuecat.product2", mockSubscriptionGroupIdentifier: "group2") product2.mockDiscount = MockDiscount() @@ -100,9 +102,8 @@ class IntroEligibilityCalculatorTests: XCTestCase { "com.revenuecat.product2", "com.revenuecat.unknownProduct"]) - calculator.checkTrialOrIntroductoryPriceEligibility(with: Data(), - productIdentifiers: Set(candidateIdentifiers)) { eligibility, - error in + calculator.checkEligibility(with: Data(), + productIdentifiers: Set(candidateIdentifiers)) { eligibility, error in completionCalled = true receivedError = error receivedEligibility = eligibility @@ -111,30 +112,28 @@ class IntroEligibilityCalculatorTests: XCTestCase { expect(completionCalled).toEventually(beTrue()) expect(receivedError).to(beNil()) expect(receivedEligibility) == [ - "com.revenuecat.product1": IntroEligibilityStatus.eligible.toNSNumber(), - "com.revenuecat.product2": IntroEligibilityStatus.ineligible.toNSNumber(), - "com.revenuecat.unknownProduct": IntroEligibilityStatus.unknown.toNSNumber(), + "com.revenuecat.product1": IntroEligibilityStatus.eligible, + "com.revenuecat.product2": IntroEligibilityStatus.ineligible, + "com.revenuecat.unknownProduct": IntroEligibilityStatus.unknown, ] } func testCheckTrialOrIntroductoryPriceEligibilityForProductWithoutIntroTrialReturnsIneligible() { var receivedError: Error? = nil - var receivedEligibility: [String: NSNumber]? = nil + var receivedEligibility: [String: IntroEligibilityStatus]? = nil var completionCalled = false let receipt = mockReceipt() mockReceiptParser.stubbedParseResult = receipt - let mockProduct = MockSKProduct(mockProductIdentifier: "com.revenuecat.product1", - mockSubscriptionGroupIdentifier: "group1") + let mockProduct = MockSK1Product(mockProductIdentifier: "com.revenuecat.product1", + mockSubscriptionGroupIdentifier: "group1") mockProduct.mockDiscount = nil mockProductsManager.stubbedProductsCompletionResult = Set([mockProduct]) let candidateIdentifiers = Set(["com.revenuecat.product1"]) - calculator.checkTrialOrIntroductoryPriceEligibility( - with: Data(), - productIdentifiers: Set(candidateIdentifiers) - ) { eligibility, error in + calculator.checkEligibility(with: Data(), + productIdentifiers: Set(candidateIdentifiers)) { eligibility, error in completionCalled = true receivedError = error receivedEligibility = eligibility @@ -143,7 +142,36 @@ class IntroEligibilityCalculatorTests: XCTestCase { expect(completionCalled).toEventually(beTrue()) expect(receivedError).to(beNil()) expect(receivedEligibility) == [ - "com.revenuecat.product1": IntroEligibilityStatus.ineligible.toNSNumber() + "com.revenuecat.product1": IntroEligibilityStatus.ineligible + ] + } + + func testCheckTrialOrIntroductoryPriceEligibilityForConsumableReturnsUnknown() { + var receivedError: Error? = nil + var receivedEligibility: [String: IntroEligibilityStatus]? = nil + var completionCalled = false + + let receipt = mockReceipt() + mockReceiptParser.stubbedParseResult = receipt + let mockProduct = MockSK1Product(mockProductIdentifier: "lifetime", + mockSubscriptionGroupIdentifier: "group1") + mockProduct.mockDiscount = nil + mockProduct.mockSubscriptionPeriod = nil + mockProductsManager.stubbedProductsCompletionResult = Set([mockProduct]) + + let candidateIdentifiers = Set(["lifetime"]) + + calculator.checkEligibility(with: Data(), + productIdentifiers: Set(candidateIdentifiers)) { eligibility, error in + completionCalled = true + receivedError = error + receivedEligibility = eligibility + } + + expect(completionCalled).toEventually(beTrue()) + expect(receivedError).to(beNil()) + expect(receivedEligibility) == [ + "lifetime": IntroEligibilityStatus.unknown ] } } diff --git a/PurchasesTests/Purchasing/OfferingsManagerTests.swift b/PurchasesTests/Purchasing/OfferingsManagerTests.swift index a2e5e8ddb0..127bb4da49 100644 --- a/PurchasesTests/Purchasing/OfferingsManagerTests.swift +++ b/PurchasesTests/Purchasing/OfferingsManagerTests.swift @@ -25,12 +25,13 @@ class OfferingsManagerTests: XCTestCase { finishTransactions: true) let mockBackend = MockBackend() let mockOfferingsFactory = MockOfferingsFactory() - let mockProductsManager = MockProductsManager() + var mockProductsManager: MockProductsManager! var offeringsManager: OfferingsManager! override func setUp() { super.setUp() - + + mockProductsManager = MockProductsManager(systemInfo: mockSystemInfo) offeringsManager = OfferingsManager(deviceCache: mockDeviceCache, operationDispatcher: mockOperationDispatcher, systemInfo: mockSystemInfo, @@ -79,7 +80,7 @@ extension OfferingsManagerTests { let obtainedOfferings = try XCTUnwrap(maybeObtainedOfferings) expect(obtainedOfferings["base"]).toNot(beNil()) expect(obtainedOfferings["base"]!.monthly).toNot(beNil()) - expect(obtainedOfferings["base"]!.monthly?.product).toNot(beNil()) + expect(obtainedOfferings["base"]!.monthly?.productDetails).toNot(beNil()) } func testOfferingsForAppUserIDReturnsNilIfFailBackendRequest() { @@ -125,7 +126,7 @@ extension OfferingsManagerTests { func testOfferingsForAppUserIDReturnsConfigurationErrorIfProductsRequestsReturnsEmpty() throws { // given mockBackend.stubbedGetOfferingsCompletionResult = (MockData.anyBackendOfferingsData, nil) - mockProductsManager.stubbedProductsCompletionResult = Set() + mockProductsManager.stubbedProductsFromOptimalStoreKitVersionWithIdentifiersCompletionResult = (Set(), ()) // when var obtainedOfferings: Offerings? @@ -232,7 +233,7 @@ extension OfferingsManagerTests { // when offeringsManager.offerings(appUserID: MockData.anyAppUserID, completion: nil) - + // then expect(self.mockDeviceCache.setOfferingsCacheTimestampToNowCount).toEventually(equal(expectedCallCount)) expect(self.mockBackend.invokedGetOfferingsForAppUserIDCount).toEventually(equal(expectedCallCount)) diff --git a/PurchasesTests/Purchasing/OfferingsTests.swift b/PurchasesTests/Purchasing/OfferingsTests.swift index 5f7cf91d18..9b4cfa30e9 100644 --- a/PurchasesTests/Purchasing/OfferingsTests.swift +++ b/PurchasesTests/Purchasing/OfferingsTests.swift @@ -21,8 +21,8 @@ class OfferingsTests: XCTestCase { let package = offeringsFactory.createPackage(withData: [ "identifier": "$rc_monthly", "platform_product_identifier": "com.myproduct.monthly" - ], products: [ - "com.myproduct.annual": SKProduct() + ], productDetailsByID: [ + "com.myproduct.annual": SK1ProductDetails(sk1Product: SK1Product()) ], offeringIdentifier: "offering") expect(package).to(beNil()) @@ -30,24 +30,26 @@ class OfferingsTests: XCTestCase { func testPackageIsCreatedIfValidProducts() { let productIdentifier = "com.myproduct.monthly" - let product = MockSKProduct(mockProductIdentifier: productIdentifier) + let product = MockSK1Product(mockProductIdentifier: productIdentifier) let packageIdentifier = "$rc_monthly" let package = offeringsFactory.createPackage(withData: [ "identifier": packageIdentifier, "platform_product_identifier": productIdentifier - ], products: [ - productIdentifier: product + ], productDetailsByID: [ + productIdentifier: SK1ProductDetails(sk1Product: product) ], offeringIdentifier: "offering") expect(package).toNot(beNil()) - expect(package?.product).to(equal(product)) + expect(package?.productDetails).to(beAnInstanceOf(SK1ProductDetails.self)) + let sk1ProductDetails = package!.productDetails as! SK1ProductDetails + expect(sk1ProductDetails.underlyingSK1Product).to(equal(product)) expect(package?.identifier).to(equal(packageIdentifier)) expect(package?.packageType).to(equal(PackageType.monthly)) } func testOfferingIsNotCreatedIfNoValidPackage() { - let products = ["com.myproduct.bad": SKProduct()] - let offering = offeringsFactory.createOffering(withProducts: products, offeringData: [ + let products = ["com.myproduct.bad": SK1ProductDetails(sk1Product: SK1Product())] + let offering = offeringsFactory.createOffering(fromProductDetailsByID: products, offeringData: [ "identifier": "offering_a", "description": "This is the base offering", "packages": [ @@ -63,12 +65,12 @@ class OfferingsTests: XCTestCase { func testOfferingIsCreatedIfValidPackages() { let products = [ - "com.myproduct.annual": MockSKProduct(mockProductIdentifier: "com.myproduct.annual"), - "com.myproduct.monthly": MockSKProduct(mockProductIdentifier: "com.myproduct.monthly") + "com.myproduct.annual": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.annual")), + "com.myproduct.monthly": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.monthly")) ] let offeringIdentifier = "offering_a" let serverDescription = "This is the base offering" - let offering = offeringsFactory.createOffering(withProducts: products, offeringData: [ + let offering = offeringsFactory.createOffering(fromProductDetailsByID: products, offeringData: [ "identifier": offeringIdentifier, "description": serverDescription, "packages": [ @@ -90,7 +92,7 @@ class OfferingsTests: XCTestCase { } func testListOfOfferingsIsNilIfNoValidOffering() { - let offerings = offeringsFactory.createOfferings(withProducts: [:], data: [ + let offerings = offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: [ "offerings": [ [ "identifier": "offering_a", @@ -117,33 +119,32 @@ class OfferingsTests: XCTestCase { func testOfferingsIsCreated() throws { let products = [ - "com.myproduct.annual": MockSKProduct(mockProductIdentifier: "com.myproduct.annual"), - "com.myproduct.monthly": MockSKProduct(mockProductIdentifier: "com.myproduct.monthly") + "com.myproduct.annual": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.annual")), + "com.myproduct.monthly": SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: "com.myproduct.monthly")) ] - let offerings = try XCTUnwrap( - offeringsFactory.createOfferings(withProducts: products, data: [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.annual"] - ] - ], - [ - "identifier": "offering_b", - "description": "This is the base offering b", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"] - ] - ], + let maybeOfferings = offeringsFactory.createOfferings(fromProductDetailsByID: products, data: [ + "offerings": [ + [ + "identifier": "offering_a", + "description": "This is the base offering", + "packages": [ + ["identifier": "$rc_six_month", + "platform_product_identifier": "com.myproduct.annual"] + ] ], - "current_offering_id": "offering_a" - ]) - ) + [ + "identifier": "offering_b", + "description": "This is the base offering b", + "packages": [ + ["identifier": "$rc_monthly", + "platform_product_identifier": "com.myproduct.monthly"] + ] + ], + ], + "current_offering_id": "offering_a" + ]) + let offerings = try XCTUnwrap(maybeOfferings) expect(offerings["offering_a"]).toNot(beNil()) expect(offerings["offering_b"]).toNot(beNil()) expect(offerings.current).to(be(offerings["offering_a"])) @@ -190,17 +191,18 @@ class OfferingsTests: XCTestCase { "offerings": [], "current_offering_id": nil ] - let maybeOfferings = offeringsFactory.createOfferings(withProducts: [:], data: data as [String : Any]) + let maybeOfferings = offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: data as [String : Any]) expect(maybeOfferings).to(beNil()) } func testCurrentOfferingWithBrokenProductReturnsNilForCurrentOfferingButContainsOtherOfferings() throws { - let products = [ - "com.myproduct.annual": MockSKProduct(mockProductIdentifier: "com.myproduct.annual"), + let productDetailsByID = [ + "com.myproduct.annual": SK1ProductDetails(sk1Product: + MockSK1Product(mockProductIdentifier: "com.myproduct.annual")), ] - let data = [ + let data: [String : Any] = [ "offerings": [ [ "identifier": "offering_a", @@ -212,8 +214,8 @@ class OfferingsTests: XCTestCase { ] ], "current_offering_id": "offering_with_broken_product" - ] as [String : Any] - let maybeOfferings = offeringsFactory.createOfferings(withProducts: products, data: data as [String : Any]) + ] + let maybeOfferings = offeringsFactory.createOfferings(fromProductDetailsByID: productDetailsByID, data: data) let offerings = try XCTUnwrap(maybeOfferings) expect(offerings.current).to(beNil()) @@ -221,7 +223,7 @@ class OfferingsTests: XCTestCase { func testBadOfferingsDataReturnsNil() { let data = [:] as [String : Any] - let offerings = offeringsFactory.createOfferings(withProducts: [:], data: data as [String : Any]) + let offerings = offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: data as [String : Any]) expect(offerings).to(beNil()) } @@ -237,24 +239,23 @@ class OfferingsTests: XCTestCase { } let productIdentifier = "com.myproduct" let products = [ - productIdentifier: MockSKProduct(mockProductIdentifier: productIdentifier) + productIdentifier: SK1ProductDetails(sk1Product: MockSK1Product(mockProductIdentifier: productIdentifier)) ] - let offerings = try XCTUnwrap( - offeringsFactory.createOfferings(withProducts: products, data: [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": identifier, - "platform_product_identifier": "com.myproduct"] - ] + let maybeOfferings = offeringsFactory.createOfferings(fromProductDetailsByID: products, data: [ + "offerings": [ + [ + "identifier": "offering_a", + "description": "This is the base offering", + "packages": [ + ["identifier": identifier, + "platform_product_identifier": "com.myproduct"] ] - ], - "current_offering_id": "offering_a" - ]) - ) + ] + ], + "current_offering_id": "offering_a" + ]) + let offerings = try XCTUnwrap(maybeOfferings) expect(offerings.current).toNot(beNil()) if (packageType == PackageType.lifetime) { expect(offerings.current?.lifetime).toNot(beNil()) diff --git a/PurchasesTests/Purchasing/ProductInfoExtractorTests.swift b/PurchasesTests/Purchasing/ProductInfoExtractorTests.swift index 019f022a9f..8fca5b168f 100644 --- a/PurchasesTests/Purchasing/ProductInfoExtractorTests.swift +++ b/PurchasesTests/Purchasing/ProductInfoExtractorTests.swift @@ -7,7 +7,7 @@ class ProductInfoExtractorTests: XCTestCase { func testExtractInfoFromProductExtractsProductIdentifier() { let productID = "cool_product" - let product = MockSKProduct(mockProductIdentifier: productID) + let product = MockSK1Product(mockProductIdentifier: productID) let productInfoExtractor = ProductInfoExtractor() let receivedProductInfo = productInfoExtractor.extractInfo(from: product) @@ -16,7 +16,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsPrice() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") let price: NSDecimalNumber = 10.99 product.mockPrice = price let productInfoExtractor = ProductInfoExtractor() @@ -27,7 +27,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsCurrencyCode() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") product.mockPriceLocale = Locale(identifier: "es_UY") let productInfoExtractor = ProductInfoExtractor() @@ -41,7 +41,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsPaymentMode() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { let mockDiscount = MockDiscount() @@ -63,7 +63,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsIntroPrice() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { let mockDiscount = MockDiscount() @@ -85,7 +85,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsNormalDuration() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { product.mockSubscriptionPeriod = SKProductSubscriptionPeriod(numberOfUnits: 2, unit: .month) @@ -104,7 +104,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductDoesNotExtractNormalDurationIfSubscriptionPeriodIsZero() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { product.mockSubscriptionPeriod = SKProductSubscriptionPeriod(numberOfUnits: 0, unit: .month) @@ -123,7 +123,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsIntroDuration() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { let mockDiscount = MockDiscount() @@ -145,7 +145,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsIntroDurationType() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 11.2, macOS 10.14.4, tvOS 11.2, *) { let mockDiscount = MockDiscount() @@ -167,7 +167,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsSubscriptionGroup() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 12.0, macCatalyst 13.0, macOS 10.14, tvOS 12.0, watchOS 6.2, *) { let group = "mock_group" product.mockSubscriptionGroupIdentifier = group @@ -186,7 +186,7 @@ class ProductInfoExtractorTests: XCTestCase { } func testExtractInfoFromProductExtractsDiscounts() { - let product = MockSKProduct(mockProductIdentifier: "cool_product") + let product = MockSK1Product(mockProductIdentifier: "cool_product") if #available(iOS 12.2, tvOS 12.2, macOS 10.13.2, *) { let mockDiscount = MockDiscount() diff --git a/PurchasesTests/Purchasing/ProductsManagerTests.swift b/PurchasesTests/Purchasing/ProductsFetcherSK1Tests.swift similarity index 68% rename from PurchasesTests/Purchasing/ProductsManagerTests.swift rename to PurchasesTests/Purchasing/ProductsFetcherSK1Tests.swift index 7204c28b1d..058420dc14 100644 --- a/PurchasesTests/Purchasing/ProductsManagerTests.swift +++ b/PurchasesTests/Purchasing/ProductsFetcherSK1Tests.swift @@ -4,36 +4,37 @@ import StoreKit @testable import RevenueCat -class ProductsManagerTests: XCTestCase { +class ProductsFetcherSK1Tests: XCTestCase { var productsRequestFactory: MockProductsRequestFactory! - var productsManager: ProductsManager! + var productsFetcherSK1: ProductsFetcherSK1! override func setUp() { super.setUp() productsRequestFactory = MockProductsRequestFactory() - productsManager = ProductsManager(productsRequestFactory: productsRequestFactory) + productsFetcherSK1 = ProductsFetcherSK1(productsRequestFactory: productsRequestFactory) } func testProductsWithIdentifiersMakesRightRequest() { let productIdentifiers = Set(["1", "2", "3"]) - productsManager.products(withIdentifiers: productIdentifiers) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { _ in } expect(self.productsRequestFactory.invokedRequestCount).toEventually(equal(1)) expect(self.productsRequestFactory.invokedRequestParameters) == productIdentifiers } - func testProductsWithIdentifiersCallsCompletionCorrectly() { + func testProductsWithIdentifiersCallsCompletionCorrectly() throws { let productIdentifiers = Set(["1", "2", "3"]) - var receivedProducts: Set? + var maybeReceivedProducts: Set? var completionCalled = false - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCalled = true - receivedProducts = products + maybeReceivedProducts = products } expect(completionCalled).toEventually(beTrue()) - expect(receivedProducts?.count) == productIdentifiers.count - let receivedProductsSet = Set(receivedProducts!.map { $0.productIdentifier }) + let receivedProducts = try XCTUnwrap(maybeReceivedProducts) + expect(receivedProducts.count) == productIdentifiers.count + let receivedProductsSet = Set(receivedProducts.map { $0.productIdentifier }) expect(receivedProductsSet) == productIdentifiers } @@ -41,10 +42,10 @@ class ProductsManagerTests: XCTestCase { let productIdentifiers = Set(["1", "2", "3"]) var completionCallCount = 0 - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCallCount += 1 - self.productsManager.products(withIdentifiers: productIdentifiers) { products in + self.productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCallCount += 1 } } @@ -57,8 +58,8 @@ class ProductsManagerTests: XCTestCase { func testProductsWithIdentifiersReturnsDoesntMakeNewRequestIfProductsAreBeingFetched() { let productIdentifiers = Set(["1", "2", "3"]) - productsManager.products(withIdentifiers: productIdentifiers) { _ in } - productsManager.products(withIdentifiers: productIdentifiers) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { _ in } expect(self.productsRequestFactory.invokedRequestCount).toEventually(equal(1)) expect(self.productsRequestFactory.invokedRequestParameters) == productIdentifiers @@ -67,15 +68,15 @@ class ProductsManagerTests: XCTestCase { func testProductsWithIdentifiersMakesNewRequestIfAtLeastOneNewProductRequested() { let firstCallProducts = Set(["1", "2", "3"]) let secondCallProducts = Set(["1", "2", "3", "4"]) - productsManager.products(withIdentifiers: firstCallProducts) { _ in } - productsManager.products(withIdentifiers: secondCallProducts) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: firstCallProducts) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: secondCallProducts) { _ in } expect(self.productsRequestFactory.invokedRequestCount).toEventually(equal(2)) expect(self.productsRequestFactory.invokedRequestParametersList) == [firstCallProducts, secondCallProducts] } func testProductsWithIdentifiersReturnsDoesntMakeNewRequestIfProductIdentifiersEmpty() { - productsManager.products(withIdentifiers: []) { _ in } + productsFetcherSK1.sk1Products(withIdentifiers: []) { _ in } expect(self.productsRequestFactory.invokedRequestCount).toEventually(equal(0)) } @@ -86,10 +87,10 @@ class ProductsManagerTests: XCTestCase { failingRequest.fails = true productsRequestFactory.stubbedRequestResult = failingRequest - var receivedProducts: Set? + var receivedProducts: Set? var completionCalled = false - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCalled = true receivedProducts = products } @@ -99,20 +100,20 @@ class ProductsManagerTests: XCTestCase { func testCacheProductCachesCorrectly() { let productIdentifiers = Set(["1", "2", "3"]) - let mockProducts:Set = Set(productIdentifiers.map { - MockSKProduct(mockProductIdentifier: $0) + let mockProducts:Set = Set(productIdentifiers.map { + MockSK1Product(mockProductIdentifier: $0) }) - - mockProducts.forEach { productsManager.cacheProduct($0) } - + + mockProducts.forEach { productsFetcherSK1.cacheProduct($0) } + var completionCallCount = 0 - var receivedProducts: Set? - - productsManager.products(withIdentifiers: productIdentifiers) { products in + var receivedProducts: Set? + + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCallCount += 1 receivedProducts = products } - + expect(completionCallCount).toEventually(equal(1)) expect(self.productsRequestFactory.invokedRequestCount).toEventually(equal(0)) expect(receivedProducts) == mockProducts @@ -126,14 +127,14 @@ class ProductsManagerTests: XCTestCase { responseTimeInSeconds: productsRequestResponseTimeInSeconds) productsRequestFactory.stubbedRequestResult = request - productsManager = ProductsManager(productsRequestFactory: productsRequestFactory, - requestTimeoutInSeconds: toleranceInSeconds) + productsFetcherSK1 = ProductsFetcherSK1(productsRequestFactory: productsRequestFactory, + requestTimeoutInSeconds: toleranceInSeconds) var completionCallCount = 0 var maybeReceivedProducts: Set? - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCallCount += 1 maybeReceivedProducts = products } @@ -153,14 +154,14 @@ class ProductsManagerTests: XCTestCase { responseTimeInSeconds: productsRequestResponseTimeInSeconds) productsRequestFactory.stubbedRequestResult = request - productsManager = ProductsManager(productsRequestFactory: productsRequestFactory, - requestTimeoutInSeconds: toleranceInSeconds) + productsFetcherSK1 = ProductsFetcherSK1(productsRequestFactory: productsRequestFactory, + requestTimeoutInSeconds: toleranceInSeconds) var completionCallCount = 0 var maybeReceivedProducts: Set? - productsManager.products(withIdentifiers: productIdentifiers) { products in + productsFetcherSK1.sk1Products(withIdentifiers: productIdentifiers) { products in completionCallCount += 1 maybeReceivedProducts = products } diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index e32bbf3796..d3b46aa2b2 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -35,13 +35,14 @@ class PurchasesTests: XCTestCase { deviceCache = MockDeviceCache(userDefaults: userDefaults) requestFetcher = MockRequestFetcher() systemInfo = try MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) - mockProductsManager = MockProductsManager() + mockProductsManager = MockProductsManager(systemInfo: systemInfo) mockOperationDispatcher = MockOperationDispatcher() - mockIntroEligibilityCalculator = MockIntroEligibilityCalculator() mockReceiptParser = MockReceiptParser() + mockIntroEligibilityCalculator = MockIntroEligibilityCalculator(productsManager: mockProductsManager, + receiptParser: mockReceiptParser) let systemInfoAttribution = try MockSystemInfo(platformFlavor: "iOS", - platformFlavorVersion: "3.2.1", - finishTransactions: true) + platformFlavorVersion: "3.2.1", + finishTransactions: true) receiptFetcher = MockReceiptFetcher(requestFetcher: requestFetcher, systemInfo: systemInfoAttribution) attributionFetcher = MockAttributionFetcher(attributionFactory: MockAttributionTypeFactory(), systemInfo: systemInfoAttribution) @@ -65,6 +66,10 @@ class PurchasesTests: XCTestCase { backend: backend, offeringsFactory: offeringsFactory, productsManager: mockProductsManager) + mockManageSubsModalHelper = MockManageSubscriptionsModalHelper(systemInfo: systemInfo, + customerInfoManager: customerInfoManager, + identityManager: identityManager) + mockBeginRefundRequestHelper = MockBeginRefundRequestHelper(systemInfo: systemInfo) } override func tearDown() { @@ -264,7 +269,7 @@ class PurchasesTests: XCTestCase { let offeringsFactory = MockOfferingsFactory() var deviceCache: MockDeviceCache! var subscriberAttributesManager: MockSubscriberAttributesManager! - let identityManager = MockIdentityManager(mockAppUserID: "app_user"); + let identityManager = MockIdentityManager(mockAppUserID: "app_user") var systemInfo: MockSystemInfo! var mockOperationDispatcher: MockOperationDispatcher! var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! @@ -274,6 +279,9 @@ class PurchasesTests: XCTestCase { var customerInfoManager: CustomerInfoManager! var mockOfferingsManager: MockOfferingsManager! var purchasesOrchestrator: PurchasesOrchestrator! + var trialOrIntroPriceEligibilityChecker: MockTrialOrIntroPriceEligibilityChecker! + var mockManageSubsModalHelper: MockManageSubscriptionsModalHelper! + var mockBeginRefundRequestHelper: MockBeginRefundRequestHelper! let purchasesDelegate = MockPurchasesDelegate() @@ -308,8 +316,15 @@ class PurchasesTests: XCTestCase { backend: backend, identityManager: identityManager, receiptParser: mockReceiptParser, - deviceCache: deviceCache) - + deviceCache: deviceCache, + manageSubscriptionsModalHelper: mockManageSubsModalHelper, + beginRefundRequestHelper: mockBeginRefundRequestHelper) + trialOrIntroPriceEligibilityChecker = MockTrialOrIntroPriceEligibilityChecker(receiptFetcher: receiptFetcher, + introEligibilityCalculator: mockIntroEligibilityCalculator, + backend: backend, + identityManager: identityManager, + operationDispatcher: mockOperationDispatcher, + productsManager: mockProductsManager) purchases = Purchases(appUserID: appUserId, requestFetcher: requestFetcher, receiptFetcher: receiptFetcher, @@ -329,7 +344,8 @@ class PurchasesTests: XCTestCase { customerInfoManager: customerInfoManager, productsManager: mockProductsManager, offeringsManager: mockOfferingsManager, - purchasesOrchestrator: purchasesOrchestrator) + purchasesOrchestrator: purchasesOrchestrator, + trialOrIntroPriceEligibilityChecker: trialOrIntroPriceEligibilityChecker) purchasesOrchestrator.maybeDelegate = purchases purchases!.delegate = purchasesDelegate @@ -435,7 +451,7 @@ class PurchasesTests: XCTestCase { let customerInfo = CustomerInfo(testData: emptyCustomerInfoData) self.backend.postReceiptCustomerInfo = customerInfo - let product = MockSKProduct(mockProductIdentifier: "product") + let product = MockSK1Product(mockProductIdentifier: "product") let payment = SKPayment(product: product) let customerInfoBeforePurchase = CustomerInfo(testData: [ @@ -487,7 +503,7 @@ class PurchasesTests: XCTestCase { let customerInfo2 = customerInfo1 - let product = MockSKProduct(mockProductIdentifier: "product") + let product = MockSK1Product(mockProductIdentifier: "product") let payment = SKPayment(product: product) let transaction = MockTransaction() @@ -534,7 +550,7 @@ class PurchasesTests: XCTestCase { ] ]) - let product = MockSKProduct(mockProductIdentifier: "product") + let product = MockSK1Product(mockProductIdentifier: "product") let payment = SKPayment(product: product) let transaction = MockTransaction() @@ -558,7 +574,7 @@ class PurchasesTests: XCTestCase { func testDelegateIsNotCalledIfBlockPassed() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -579,7 +595,7 @@ class PurchasesTests: XCTestCase { func testIsAbleToFetchProducts() { setupPurchases() - var products: [SKProduct]? + var products: [SK1Product]? let productIdentifiers = ["com.product.id1", "com.product.id2"] purchases!.getProducts(productIdentifiers) { (newProducts) in products = newProducts @@ -596,7 +612,7 @@ class PurchasesTests: XCTestCase { func testAddsPaymentToWrapper() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -607,7 +623,7 @@ class PurchasesTests: XCTestCase { func testPurchaseProductCachesProduct() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -629,7 +645,7 @@ class PurchasesTests: XCTestCase { func testTransitioningToPurchasing() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -645,7 +661,7 @@ class PurchasesTests: XCTestCase { func testTransitioningToPurchasedSendsToBackend() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -665,7 +681,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsAsRestoreWhenAnon() { setupAnonPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -687,7 +703,7 @@ class PurchasesTests: XCTestCase { setupAnonPurchases() var deprecated = purchases.deprecated deprecated.allowSharingAppStoreAccount = false - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -709,7 +725,7 @@ class PurchasesTests: XCTestCase { setupPurchases() var deprecated = purchases.deprecated deprecated.allowSharingAppStoreAccount = true - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -729,7 +745,7 @@ class PurchasesTests: XCTestCase { func testFinishesTransactionsIfSentToBackendCorrectly() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -752,7 +768,7 @@ class PurchasesTests: XCTestCase { func testDoesntFinishTransactionsIfFinishingDisabled() { setupPurchases() self.purchases?.finishTransactions = false - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -827,7 +843,7 @@ class PurchasesTests: XCTestCase { func testFetchesProductInfoIfNotCached() { systemInfo.stubbedIsApplicationBackgrounded = true setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") let transaction = MockTransaction() storeKitWrapper.payment = SKPayment(product: product); @@ -857,7 +873,7 @@ class PurchasesTests: XCTestCase { func testAfterSendingDoesntFinishTransactionIfBackendError() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -877,7 +893,7 @@ class PurchasesTests: XCTestCase { func testAfterSendingFinishesFromBackendErrorIfAppropriate() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -898,7 +914,7 @@ class PurchasesTests: XCTestCase { func testNotifiesIfTransactionFailsFromBackend() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -920,7 +936,7 @@ class PurchasesTests: XCTestCase { func testNotifiesIfTransactionFailsFromStoreKit() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedError: Error? self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error @@ -942,7 +958,7 @@ class PurchasesTests: XCTestCase { func testCallsDelegateAfterBackendResponse() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var customerInfo: CustomerInfo? var receivedError: Error? @@ -987,7 +1003,7 @@ class PurchasesTests: XCTestCase { func testCompletionBlockOnlyCalledOnce() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var callCount = 0 @@ -1010,8 +1026,8 @@ class PurchasesTests: XCTestCase { func testCompletionBlockNotCalledForDifferentProducts() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - let otherProduct = MockSKProduct(mockProductIdentifier: "com.product.id2") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") + let otherProduct = MockSK1Product(mockProductIdentifier: "com.product.id2") var callCount = 0 @@ -1033,7 +1049,7 @@ class PurchasesTests: XCTestCase { func testCallingPurchaseWhileSameProductPendingIssuesError() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") // First one "works" self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in @@ -1418,8 +1434,8 @@ class PurchasesTests: XCTestCase { func testCallsShouldAddPromoPaymentDelegateMethod() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") - let payment = SKPayment.init() + let product = MockSK1Product(mockProductIdentifier: "mock_product") + let payment = SKPayment() _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, shouldAddStorePayment: payment, @@ -1430,8 +1446,8 @@ class PurchasesTests: XCTestCase { func testShouldAddPromoPaymentDelegateMethodReturnsFalse() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") - let payment = SKPayment.init() + let product = MockSK1Product(mockProductIdentifier: "mock_product") + let payment = SKPayment() let result = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, shouldAddStorePayment: payment, @@ -1442,7 +1458,7 @@ class PurchasesTests: XCTestCase { func testPromoPaymentDelegateMethodMakesRightCalls() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") + let product = MockSK1Product(mockProductIdentifier: "mock_product") let payment = SKPayment.init(product: product) _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, @@ -1465,7 +1481,7 @@ class PurchasesTests: XCTestCase { func testPromoPaymentDelegateMethodCachesProduct() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") + let product = MockSK1Product(mockProductIdentifier: "mock_product") let payment = SKPayment.init(product: product) _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, @@ -1487,7 +1503,7 @@ class PurchasesTests: XCTestCase { func testDeferBlockMakesPayment() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") + let product = MockSK1Product(mockProductIdentifier: "mock_product") let payment = SKPayment.init(product: product) guard let storeKitWrapperDelegate = storeKitWrapper.delegate else { @@ -1528,14 +1544,8 @@ class PurchasesTests: XCTestCase { setupPurchases() purchases!.checkTrialOrIntroductoryPriceEligibility([]) { (eligibilities) in } - } - func testGetEligibilitySendsAReceipt() { - setupPurchases() - purchases!.checkTrialOrIntroductoryPriceEligibility([]) { (eligibilities) in - } - - expect(self.receiptFetcher.receiptDataCalled).to(beTrue()) + expect(self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore).to(beTrue()) } func testFetchVersionSendsAReceiptIfNoVersion() { @@ -1596,7 +1606,7 @@ class PurchasesTests: XCTestCase { "other_purchases": [:] ]]); - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -1801,9 +1811,11 @@ class PurchasesTests: XCTestCase { func testProductInfoIsCachedForOfferings() { setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = + (offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: [:]), nil) self.purchases?.getOfferings { (newOfferings, _) in - let product = newOfferings!["base"]!.monthly!.product; + let productDetails = newOfferings!["base"]!.monthly!.productDetails; + let product = (productDetails as! SK1ProductDetails).underlyingSK1Product self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2062,7 +2074,7 @@ class PurchasesTests: XCTestCase { } private func makeAPurchase() { - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") guard let purchases = purchases else { fatalError("purchases is not initialized") } purchases.purchase(product: product) { _,_,_,_ in } @@ -2099,7 +2111,7 @@ class PurchasesTests: XCTestCase { func testUserCancelledFalseIfPurchaseSuccessful() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedUserCancelled: Bool? self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in @@ -2116,7 +2128,7 @@ class PurchasesTests: XCTestCase { func testUnknownErrorCurrentlySubscribedIsParsedCorrectly() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedUserCancelled: Bool? var receivedError: NSError? var receivedUnderlyingError: NSError? @@ -2155,7 +2167,7 @@ class PurchasesTests: XCTestCase { func testUserCancelledTrueIfPurchaseCancelled() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedUserCancelled: Bool? var receivedError: NSError? var receivedUnderlyingError: NSError? @@ -2184,7 +2196,7 @@ class PurchasesTests: XCTestCase { setupPurchases() self.receiptFetcher.shouldReturnReceipt = false - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedUserCancelled: Bool? var receivedError: NSError? @@ -2205,7 +2217,7 @@ class PurchasesTests: XCTestCase { func testDeferBlockCallsCompletionBlockAfterPurchaseCompletes() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "mock_product") + let product = MockSK1Product(mockProductIdentifier: "mock_product") let payment = SKPayment.init(product: product) _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, @@ -2239,7 +2251,7 @@ class PurchasesTests: XCTestCase { func testAddsDiscountToWrapper() { if #available(iOS 12.2, tvOS 12.2, macOS 10.14.4, *) { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") let discount = SKPaymentDiscount.init(identifier: "discount", keyIdentifier: "TIKAMASALA1", nonce: UUID(), signature: "Base64 encoded signature", timestamp: NSNumber(value: Int64(123413232131))) self.purchases?.purchase(product: product, discount: discount) { (tx, info, error, userCancelled) in @@ -2255,7 +2267,7 @@ class PurchasesTests: XCTestCase { func testPaymentDiscountForProductDiscountCreatesDiscount() { if #available(iOS 12.2, tvOS 12.2, macOS 10.14.4, *) { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") product.mockSubscriptionGroupIdentifier = "productGroup" let discountIdentifier = "id" @@ -2293,7 +2305,7 @@ class PurchasesTests: XCTestCase { func testPaymentDiscountForProductDiscountWithNoIDReturnsError() { if #available(iOS 12.2, tvOS 12.2, macOS 10.14.4, *) { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") let signature = "firma" let keyIdentifier = "key_id" let nonce = UUID() @@ -2319,7 +2331,7 @@ class PurchasesTests: XCTestCase { func testPaymentDiscountForProductDiscountCallsCompletionWithErrorIfReceiptNil() { if #available(iOS 12.2, tvOS 12.2, macOS 10.14.4, *) { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") product.mockSubscriptionGroupIdentifier = "productGroup" let discountIdentifier = "id" @@ -2348,7 +2360,7 @@ class PurchasesTests: XCTestCase { func testPaymentDiscountForProductDiscountCallsCompletionWithErrorIfReceiptEmpty() { if #available(iOS 12.2, tvOS 12.2, macOS 10.14.4, *) { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") product.mockSubscriptionGroupIdentifier = "productGroup" let discountIdentifier = "id" @@ -2457,7 +2469,7 @@ class PurchasesTests: XCTestCase { func testObserverModeSetToFalseSetFinishTransactions() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2479,7 +2491,7 @@ class PurchasesTests: XCTestCase { func testDoesntFinishTransactionsIfObserverModeIsSet() throws { try setupPurchasesObserverModeOn() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2501,7 +2513,7 @@ class PurchasesTests: XCTestCase { func testRestoredPurchasesArePosted() throws { try setupPurchasesObserverModeOn() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2518,7 +2530,7 @@ class PurchasesTests: XCTestCase { func testNilProductIdentifier() { setupPurchases() - let product = SKProduct() + let product = SK1Product() var receivedError: Error? self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error @@ -2529,7 +2541,7 @@ class PurchasesTests: XCTestCase { func testNoCrashIfPaymentIsMissing() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2557,7 +2569,8 @@ class PurchasesTests: XCTestCase { func testPostsOfferingIfPurchasingPackage() { setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = + (offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: [:]), nil) self.purchases!.getOfferings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! self.purchases!.purchase(package: package) { (tx, info, error, userCancelled) in @@ -2578,8 +2591,8 @@ class PurchasesTests: XCTestCase { expect(self.backend.postReceiptDataCalled).to(beTrue()) expect(self.backend.postedReceiptData).toNot(beNil()) - expect(self.backend.postedProductID).to(equal(package.product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(package.product.price)) + expect(self.backend.postedProductID).to(equal(package.productDetails.productIdentifier)) + expect(self.backend.postedPrice) == package.productDetails.price as NSDecimalNumber expect(self.backend.postedOfferingIdentifier).to(equal("base")) expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) } @@ -2590,7 +2603,7 @@ class PurchasesTests: XCTestCase { setupPurchases() var receivedError: NSError? = nil var receivedPaymentDiscount: SKPaymentDiscount? = nil - let product = MockSKProduct(mockProductIdentifier: "Tacos_with_free_guac") + let product = MockSK1Product(mockProductIdentifier: "Tacos_with_free_guac") let discount = MockDiscount() purchases.paymentDiscount(forProductDiscount: discount, product: product) { discount, error in @@ -2607,7 +2620,8 @@ class PurchasesTests: XCTestCase { setupPurchases() var receivedError: NSError? = nil var secondCompletionCalled = false - mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = + (offeringsFactory.createOfferings(fromProductDetailsByID: [:], data: [:]), nil) self.purchases!.getOfferings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! self.purchases!.purchase(package: package) { _,_,_,_ in @@ -2657,9 +2671,9 @@ class PurchasesTests: XCTestCase { } func testProductIsRemovedButPresentInTheQueuedTransaction() { - self.mockProductsManager.stubbedProductsCompletionResult = Set() + self.mockProductsManager.stubbedProductsCompletionResult = Set() setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "product") + let product = MockSK1Product(mockProductIdentifier: "product") let customerInfoBeforePurchase = CustomerInfo(testData: [ "request_date": "2019-08-16T10:30:42Z", @@ -2698,7 +2712,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsObserverModeWhenObserverMode() throws { try setupPurchasesObserverModeOn() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2718,7 +2732,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsObserverModeOffWhenObserverModeOff() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2843,7 +2857,7 @@ class PurchasesTests: XCTestCase { func testNotifiesIfTransactionIsDeferredFromStoreKit() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") var receivedError: NSError? self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error as NSError? diff --git a/PurchasesTests/Purchasing/RCIntroEligibilityTests.swift b/PurchasesTests/Purchasing/RCIntroEligibilityTests.swift index e69abf43ff..e69de29bb2 100644 --- a/PurchasesTests/Purchasing/RCIntroEligibilityTests.swift +++ b/PurchasesTests/Purchasing/RCIntroEligibilityTests.swift @@ -1,39 +0,0 @@ -// -// IntroEligibilityTests.swift -// PurchasesTests -// -// Created by Andrés Boedo on 7/1/20. -// Copyright © 2020 Purchases. All rights reserved. -// - -import XCTest -import Nimble - -@testable import RevenueCat - -class IntroEligibilityTests: XCTestCase { - func testInitWithEligibilityStatusCodeUnknown() throws { - expect(try self.create(.unknown).status) == IntroEligibilityStatus.unknown - } - - func testInitWithEligibilityStatusCodeIneligible() throws { - expect(try self.create(.ineligible).status) == IntroEligibilityStatus.ineligible - } - - func testInitWithEligibilityStatusCodeEligible() throws { - expect(try self.create(.eligible).status) == IntroEligibilityStatus.eligible - } - - func testInitWithEligibilityStatusCodeFailsIfInvalid() { - expect(try IntroEligibility(eligibilityStatusCode: -1)).to( - throwError(closure: { expect($0.localizedDescription).to(equal("😿 Invalid status code: -1"))}) - ) - expect(try IntroEligibility(eligibilityStatusCode: 3)).to( - throwError(closure: { expect($0.localizedDescription).to(equal("😿 Invalid status code: 3"))}) - ) - } - - private func create(_ code: IntroEligibilityStatus) throws -> IntroEligibility { - return try IntroEligibility(eligibilityStatusCode: NSNumber(value: code.rawValue)) - } -} diff --git a/PurchasesTests/Purchasing/StoreKitWrapperTests.swift b/PurchasesTests/Purchasing/StoreKitWrapperTests.swift index 8a74c62073..393c8e8e2b 100644 --- a/PurchasesTests/Purchasing/StoreKitWrapperTests.swift +++ b/PurchasesTests/Purchasing/StoreKitWrapperTests.swift @@ -59,11 +59,11 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { } var promoPayment: SKPayment? - var promoProduct: SKProduct? + var promoProduct: SK1Product? var shouldAddPromo = false func storeKitWrapper(_ storeKitWrapper: StoreKitWrapper, shouldAddStorePayment payment: SKPayment, - for product: SKProduct) -> Bool { + for product: SK1Product) -> Bool { promoPayment = payment promoProduct = product return shouldAddPromo @@ -80,7 +80,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { } func testAddsPaymentsToTheQueue() { - let payment = SKPayment.init(product: SKProduct.init()) + let payment = SKPayment.init(product: SK1Product()) wrapper?.add(payment) @@ -88,7 +88,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { } func testCallsDelegateWhenTransactionsAreUpdated() { - let payment = SKPayment.init(product: SKProduct.init()) + let payment = SKPayment.init(product: SK1Product()) wrapper?.add(payment) let transaction = MockTransaction() @@ -101,7 +101,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { @available(iOS 11.0, tvOS 11.0, macOS 11.0, macCatalyst 14.0, *) func testCallsDelegateWhenPromoPurchaseIsAvailable() { - let product = SKProduct.init(); + let product = SK1Product(); let payment = SKPayment.init(product: product) _ = wrapper?.paymentQueue(paymentQueue, shouldAddStorePayment: payment, for: product) @@ -111,7 +111,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { @available(iOS 11.0, tvOS 11.0, macOS 11.0, macCatalyst 14.0, *) func testPromoDelegateMethodPassesBackReturnValueFromOwnDelegate() { - let product = SKProduct.init(); + let product = SK1Product(); let payment = SKPayment.init(product: product) shouldAddPromo = (arc4random() % 2 == 0) as Bool @@ -122,10 +122,10 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { } func testCallsDelegateOncePerTransaction() { - let payment1 = SKPayment.init(product: SKProduct.init()) + let payment1 = SKPayment.init(product: SK1Product()) wrapper?.add(payment1) - let payment2 = SKPayment.init(product: SKProduct.init()) + let payment2 = SKPayment.init(product: SK1Product()) wrapper?.add(payment2) let transaction1 = MockTransaction() @@ -140,7 +140,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { } func testFinishesTransactions() { - let payment = SKPayment.init(product: SKProduct.init()) + let payment = SKPayment.init(product: SK1Product()) wrapper?.add(payment) let transaction = MockTransaction() @@ -155,9 +155,9 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { func testCallsRemovedTransactionDelegateMethod() { let transaction1 = MockTransaction() - transaction1.mockPayment = SKPayment.init(product: SKProduct.init()) + transaction1.mockPayment = SKPayment.init(product: SK1Product()) let transaction2 = MockTransaction() - transaction2.mockPayment = SKPayment.init(product: SKProduct.init()) + transaction2.mockPayment = SKPayment.init(product: SK1Product()) wrapper?.paymentQueue(paymentQueue, removedTransactions: [transaction1, transaction2]) @@ -194,7 +194,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { guard let wrapper = wrapper else { fatalError("wrapper is not initialized!") } let productId = "mySuperProduct" - let mockProduct = MockSKProduct(mockProductIdentifier: productId) + let mockProduct = MockSK1Product(mockProductIdentifier: productId) let payment = wrapper.payment(withProduct: mockProduct) expect(payment.productIdentifier) == productId } @@ -203,7 +203,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { func testPaymentWithProductSetsSimulatesAskToBuyInSandbox() { guard let wrapper = wrapper else { fatalError("wrapper is not initialized!") } - let mockProduct = MockSKProduct(mockProductIdentifier: "mySuperProduct") + let mockProduct = MockSK1Product(mockProductIdentifier: "mySuperProduct") StoreKitWrapper.simulatesAskToBuyInSandbox = false let payment1 = wrapper.payment(withProduct: mockProduct) @@ -221,7 +221,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { let productId = "mySuperProduct" let discountId = "mySuperDiscount" - let mockProduct = MockSKProduct(mockProductIdentifier: productId) + let mockProduct = MockSK1Product(mockProductIdentifier: productId) let mockDiscount = MockPaymentDiscount(mockIdentifier: discountId) let payment = wrapper.payment(withProduct: mockProduct, discount: mockDiscount) expect(payment.productIdentifier) == productId @@ -233,7 +233,7 @@ class StoreKitWrapperTests: XCTestCase, StoreKitWrapperDelegate { if #available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) { guard let wrapper = wrapper else { fatalError("wrapper is not initialized!") } - let mockProduct = MockSKProduct(mockProductIdentifier: "mySuperProduct") + let mockProduct = MockSK1Product(mockProductIdentifier: "mySuperProduct") let mockDiscount = MockPaymentDiscount(mockIdentifier: "mySuperDiscount") StoreKitWrapper.simulatesAskToBuyInSandbox = false diff --git a/PurchasesTests/StoreKitExtensions/SKPaymentTransactionExtensionsTests.swift b/PurchasesTests/StoreKitExtensions/SKPaymentTransactionExtensionsTests.swift index 3f81cd6dc8..ecf6fd92bd 100644 --- a/PurchasesTests/StoreKitExtensions/SKPaymentTransactionExtensionsTests.swift +++ b/PurchasesTests/StoreKitExtensions/SKPaymentTransactionExtensionsTests.swift @@ -32,7 +32,7 @@ class SKPaymentTransactionExtensionsTests: XCTestCase { func testProductIdentifierFromAnyTransaction() { let expectedProductIdentifier = "com.product.id1" - let product = MockSKProduct(mockProductIdentifier: expectedProductIdentifier) + let product = MockSK1Product(mockProductIdentifier: expectedProductIdentifier) let payment = SKPayment(product: product) let transaction = MockTransaction() transaction.mockPayment = payment diff --git a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index b1ff455bdf..c2141d1d37 100644 --- a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -20,7 +20,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { var mockReceiptFetcher: MockReceiptFetcher! let mockRequestFetcher = MockRequestFetcher() - let mockProductsManager = MockProductsManager() + var mockProductsManager: MockProductsManager! let mockBackend = MockBackend() let mockStoreKitWrapper = MockStoreKitWrapper() let mockNotificationCenter = MockNotificationCenter() @@ -55,6 +55,8 @@ class PurchasesSubscriberAttributesTests: XCTestCase { ]] var mockOfferingsManager: MockOfferingsManager! + var mockManageSubsModalHelper: MockManageSubscriptionsModalHelper! + var mockBeginRefundRequestHelper: MockBeginRefundRequestHelper! var purchases: Purchases! @@ -73,11 +75,13 @@ class PurchasesSubscriberAttributesTests: XCTestCase { subscriberAttributeWeight.key: subscriberAttributeWeight ] self.mockOperationDispatcher = MockOperationDispatcher() - self.mockIntroEligibilityCalculator = MockIntroEligibilityCalculator() self.mockReceiptParser = MockReceiptParser() + self.mockProductsManager = MockProductsManager(systemInfo: systemInfo) + self.mockIntroEligibilityCalculator = MockIntroEligibilityCalculator(productsManager: mockProductsManager, + receiptParser: mockReceiptParser) let systemInfoAttribution = try MockSystemInfo(platformFlavor: "iOS", - platformFlavorVersion: "3.2.1", - finishTransactions: true) + platformFlavorVersion: "3.2.1", + finishTransactions: true) self.mockAttributionFetcher = MockAttributionFetcher(attributionFactory: AttributionTypeFactory(), systemInfo: systemInfoAttribution) self.mockSubscriberAttributesManager = MockSubscriberAttributesManager( @@ -99,8 +103,12 @@ class PurchasesSubscriberAttributesTests: XCTestCase { systemInfo: systemInfo, backend: mockBackend, offeringsFactory: MockOfferingsFactory(), - productsManager: MockProductsManager()) + productsManager: mockProductsManager) self.mockReceiptFetcher = MockReceiptFetcher(requestFetcher: mockRequestFetcher, systemInfo: systemInfoAttribution) + self.mockManageSubsModalHelper = MockManageSubscriptionsModalHelper(systemInfo: systemInfo, + customerInfoManager: customerInfoManager, + identityManager: mockIdentityManager) + self.mockBeginRefundRequestHelper = MockBeginRefundRequestHelper(systemInfo: systemInfo) } override func tearDown() { @@ -122,7 +130,16 @@ class PurchasesSubscriberAttributesTests: XCTestCase { backend: mockBackend, identityManager: mockIdentityManager, receiptParser: mockReceiptParser, - deviceCache: mockDeviceCache) + deviceCache: mockDeviceCache, + manageSubscriptionsModalHelper: mockManageSubsModalHelper, + beginRefundRequestHelper: mockBeginRefundRequestHelper) + let trialOrIntroductoryPriceEligibilityChecker = + TrialOrIntroPriceEligibilityChecker(receiptFetcher: mockReceiptFetcher, + introEligibilityCalculator: mockIntroEligibilityCalculator, + backend: mockBackend, + identityManager: mockIdentityManager, + operationDispatcher: mockOperationDispatcher, + productsManager: mockProductsManager) purchases = Purchases(appUserID: mockIdentityManager.currentAppUserID, requestFetcher: mockRequestFetcher, receiptFetcher: mockReceiptFetcher, @@ -142,7 +159,8 @@ class PurchasesSubscriberAttributesTests: XCTestCase { customerInfoManager: customerInfoManager, productsManager: mockProductsManager, offeringsManager: mockOfferingsManager, - purchasesOrchestrator: purchasesOrchestrator) + purchasesOrchestrator: purchasesOrchestrator, + trialOrIntroPriceEligibilityChecker: trialOrIntroductoryPriceEligibilityChecker) purchasesOrchestrator.maybeDelegate = purchases purchases!.delegate = purchasesDelegate Purchases.setDefaultInstance(purchases!) @@ -569,7 +587,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptMarksSubscriberAttributesSyncedIfBackendSuccessful() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes @@ -595,7 +613,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptMarksSubscriberAttributesSyncedIfBackendSuccessfullySynced() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes @@ -623,7 +641,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptDoesntMarkSubscriberAttributesSyncedIfBackendNotSuccessfullySynced() { setupPurchases() - let product = MockSKProduct(mockProductIdentifier: "com.product.id1") + let product = MockSK1Product(mockProductIdentifier: "com.product.id1") self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes diff --git a/PurchasesTests/Support/BeginRefundRequestHelperTests.swift b/PurchasesTests/Support/BeginRefundRequestHelperTests.swift new file mode 100644 index 0000000000..84423af2ce --- /dev/null +++ b/PurchasesTests/Support/BeginRefundRequestHelperTests.swift @@ -0,0 +1,159 @@ +// +// 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 +// +// BeginRefundRequestHelperTests.swift +// +// Created by Madeline Beyl on 10/15/21. + +import Foundation +import Nimble +import StoreKit +import XCTest + +@testable import RevenueCat + +class BeginRefundRequestHelperTests: XCTestCase { + + private var systemInfo: MockSystemInfo! + private var helper: BeginRefundRequestHelper! + private let mockProductID = "1234" + + @available(iOS 15.0, macCatalyst 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + private lazy var sk2Helper = MockSK2BeginRefundRequestHelper() + + override func setUpWithError() throws { + try super.setUpWithError() + systemInfo = try MockSystemInfo(platformFlavor: "", platformFlavorVersion: "", finishTransactions: true) + helper = BeginRefundRequestHelper(systemInfo: systemInfo) + + if #available(iOS 15.0, macCatalyst 15.0, *) { + helper.sk2Helper = sk2Helper + } + } + + func testBeginRefundRequestFatalErrorIfNotIosOrCatalyst() { + #if os(watchOS) || os(macOS) || os(tvOS) + expectFatalError(expectedMessage: Strings.purchase.begin_refund_request_unsupported.description) { + helper.beginRefundRequest(productID: mockProductID) { result in } + } + #endif + } + +#if os(iOS) || targetEnvironment(macCatalyst) + func testBeginRefundRequestFailsAndPassesErrorThroughIfPurchasesUnverified() throws { + guard #available(iOS 15.0, macCatalyst 15.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let expectedError = ErrorUtils.beginRefundRequestError(withMessage: "test") + + sk2Helper.transactionVerified = false + sk2Helper.maybeMockSK2Error = expectedError + + + var callbackCalled = false + var receivedResult: Result? + + helper.beginRefundRequest(productID: mockProductID) { result in + callbackCalled = true + receivedResult = result + } + + expect(callbackCalled).toEventually(beTrue()) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + expect(nonNilReceivedResult).to(beFailure { error in + expect(error).to(matchError(expectedError)) + }) + expect(self.sk2Helper.verifyTransactionCalled).to(beTrue()) + + // confirm we don't call refund request method if transaction not verified + expect(self.sk2Helper.refundRequestCalled).to(beFalse()) + } + + func testBeginRefundRequestCallsStoreKitRefundRequestMethodForVerifiedTransaction() throws { + guard #available(iOS 15.0, macCatalyst 15.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + sk2Helper.maybeMockSK2Status = StoreKit.Transaction.RefundRequestStatus.success + sk2Helper.transactionVerified = true + + var callbackCalled = false + var receivedResult: Result? + + helper.beginRefundRequest(productID: mockProductID) { result in + callbackCalled = true + receivedResult = result + } + + expect(callbackCalled).toEventually(beTrue()) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + let expectedStatus = RefundRequestStatus.success + expect(nonNilReceivedResult).to(beSuccess { status in + expect(status) == expectedStatus + }) + expect(self.sk2Helper.verifyTransactionCalled).to(beTrue()) + expect(self.sk2Helper.refundRequestCalled).to(beTrue()) + } + + func testBeginRefundReturnsSuccessOnStoreKitSuccess() throws { + guard #available(iOS 15.0, macCatalyst 15.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + sk2Helper.maybeMockSK2Status = StoreKit.Transaction.RefundRequestStatus.success + + var callbackCalled = false + var receivedResult: Result? + + helper.beginRefundRequest(productID: mockProductID) { result in + callbackCalled = true + receivedResult = result + } + + expect(callbackCalled).toEventually(beTrue()) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + let expectedStatus = RefundRequestStatus.success + expect(nonNilReceivedResult).to(beSuccess { status in + expect(status) == expectedStatus + }) + } + + func testBeginRefundReturnsFailureOnStoreKitRefundRequestFailure() throws { + guard #available(iOS 15.0, macCatalyst 15.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let expectedError = ErrorUtils.beginRefundRequestError(withMessage: "test") + sk2Helper.maybeMockSK2Error = expectedError + + var callbackCalled = false + var receivedResult: Result? + + helper.beginRefundRequest(productID: mockProductID) { result in + callbackCalled = true + receivedResult = result + } + + expect(callbackCalled).toEventually(beTrue()) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + expect(nonNilReceivedResult).to(beFailure { error in + expect(error).to(matchError(expectedError)) + }) + } + + func testBeginRefundMapsSK2FailuresCorrectly(){ + + } +#endif + +} diff --git a/PurchasesTests/RequiresXcode13/Support/ManageSubscriptionsModalHelperTests.swift b/PurchasesTests/Support/ManageSubscriptionsModalHelperTests.swift similarity index 61% rename from PurchasesTests/RequiresXcode13/Support/ManageSubscriptionsModalHelperTests.swift rename to PurchasesTests/Support/ManageSubscriptionsModalHelperTests.swift index aa610a8263..90ac244d41 100644 --- a/PurchasesTests/RequiresXcode13/Support/ManageSubscriptionsModalHelperTests.swift +++ b/PurchasesTests/Support/ManageSubscriptionsModalHelperTests.swift @@ -22,10 +22,10 @@ import XCTest class ManageSubscriptionsModalHelperTests: XCTestCase { private var systemInfo: MockSystemInfo! - private var purchaserInfoManager: MockPurchaserInfoManager! + private var customerInfoManager: MockCustomerInfoManager! private var identityManager: MockIdentityManager! private var helper: ManageSubscriptionsModalHelper! - private let mockPurchaserInfoData: [String: Any] = [ + private let mockCustomerInfoData: [String: Any] = [ "request_date": "2018-12-21T02:40:36Z", "subscriber": [ "original_app_user_id": "app_user_id", @@ -38,23 +38,26 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { ] - override func setUp() { - systemInfo = try! MockSystemInfo(platformFlavor: "", platformFlavorVersion: "", finishTransactions: true) - purchaserInfoManager = MockPurchaserInfoManager(operationDispatcher: MockOperationDispatcher(), - deviceCache: MockDeviceCache(), - backend: MockBackend(), - systemInfo: systemInfo) + override func setUpWithError() throws { + try super.setUpWithError() + systemInfo = try MockSystemInfo(platformFlavor: "", platformFlavorVersion: "", finishTransactions: true) + customerInfoManager = MockCustomerInfoManager(operationDispatcher: MockOperationDispatcher(), + deviceCache: MockDeviceCache(), + backend: MockBackend(), + systemInfo: systemInfo) identityManager = MockIdentityManager(mockAppUserID: "appUserID") helper = ManageSubscriptionsModalHelper(systemInfo: systemInfo, - purchaserInfoManager: purchaserInfoManager, + customerInfoManager: customerInfoManager, identityManager: identityManager) } func testShowManageSubscriptionModalMakesRightCalls() throws { - guard #available(iOS 15.0, *) else { return } + guard #available(iOS 15.0, *) else { throw XCTSkip("Required API is not available for this test.") } // given var callbackCalled = false - purchaserInfoManager.stubbedPurchaserInfo = PurchaserInfo(data: mockPurchaserInfoData) + // swiftlint:disable force_try + customerInfoManager.stubbedCustomerInfo = try CustomerInfo(data: mockCustomerInfoData) + // swiftlint:disable force_try // when helper.showManageSubscriptionModal { result in @@ -63,7 +66,7 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { // then expect(callbackCalled).toEventually(beTrue()) - expect(self.purchaserInfoManager.invokedPurchaserInfo) == true + expect(self.customerInfoManager.invokedCustomerInfo) == true // we'd ideally also patch the UIApplication (or NSWorkspace for mac), as well as // AppStore, and check for the calls in those, but it gets very tricky. @@ -73,8 +76,8 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { #if os(iOS) // given var callbackCalled = false - var receivedResult: Result? - purchaserInfoManager.stubbedPurchaserInfo = PurchaserInfo(data: mockPurchaserInfoData) + var receivedResult: Result? + customerInfoManager.stubbedCustomerInfo = try CustomerInfo(data: mockCustomerInfoData) // when helper.showManageSubscriptionModal { result in @@ -84,11 +87,11 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { // then expect(callbackCalled).toEventually(beTrue()) - let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) if #available(iOS 15.0, *) { // in tests in iOS 15, this method always fails, since the currentWindow scene can't be obtained. expect(nonNilReceivedResult).to(beFailure { error in - expect(error).to(matchError(ManageSubscriptionsModalError.couldntGetWindowScene)) + expect(error).to(matchError(ErrorCode.storeProblemError)) }) } else { expect(nonNilReceivedResult).to(beSuccess()) @@ -101,8 +104,8 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { #if os(macOS) // given var callbackCalled = false - var receivedResult: Result? - purchaserInfoManager.stubbedPurchaserInfo = PurchaserInfo(data: mockPurchaserInfoData) + var receivedResult: Result? + customerInfoManager.stubbedCustomerInfo = CustomerInfo(data: mockCustomerInfoData) // when helper.showManageSubscriptionModal { result in @@ -112,16 +115,16 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { // then expect(callbackCalled).toEventually(beTrue()) - let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) expect(nonNilReceivedResult).to(beSuccess()) #endif } - func testShowManageSubscriptionModalFailsIfCouldntGetPurchaserInfo() throws { + func testShowManageSubscriptionModalFailsIfCouldntGetCustomerInfo() throws { // given var callbackCalled = false - var receivedResult: Result? - purchaserInfoManager.stubbedError = NSError(domain: RCPurchasesErrorCodeDomain, code: 123, userInfo: nil) + var receivedResult: Result? + customerInfoManager.stubbedError = NSError(domain: RCPurchasesErrorCodeDomain, code: 123, userInfo: nil) // when helper.showManageSubscriptionModal { result in @@ -131,10 +134,10 @@ class ManageSubscriptionsModalHelperTests: XCTestCase { // then expect(callbackCalled).toEventually(beTrue()) - let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) - let expectedErrorMessage = "Failed to get managemementURL from PurchaserInfo. " + + let nonNilReceivedResult: Result = try XCTUnwrap(receivedResult) + let expectedErrorMessage = "Failed to get managemementURL from CustomerInfo. " + "Details: The operation couldn’t be completed" - let expectedError = ManageSubscriptionsModalError.couldntGetPurchaserInfo(message: expectedErrorMessage) + let expectedError = ErrorUtils.customerInfoError(withMessage: expectedErrorMessage, error: customerInfoManager.stubbedError) expect(nonNilReceivedResult).to(beFailure { error in expect(error).to(matchError(expectedError)) }) diff --git a/RevenueCat.podspec b/RevenueCat.podspec index a2b5edf1c7..6a80687b82 100644 --- a/RevenueCat.podspec +++ b/RevenueCat.podspec @@ -16,15 +16,18 @@ Pod::Spec.new do |s| s.framework = 'StoreKit' s.swift_version = '5.5' - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.12' + # todo: deployment_target set to 12.0 instead of 9.0 temporarily for iOS due to a known issue in + # Xcode-beta 5, where swift libraries fail to build for iOS targets that use armv7. + # See issue 74120874 in the release notes: + # https://developer.apple.com/documentation/xcode-release-notes/xcode-13-beta-release-notes + s.ios.deployment_target = '12.0' s.watchos.deployment_target = '6.2' s.tvos.deployment_target = '9.0' - + s.osx.deployment_target = '10.12' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.source_files = 'Purchases/**/*.swift' - s.exclude_files = 'RequiresXcode13/Support/**/*' end diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index f32921b980..55a9c137ed 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -13,9 +13,12 @@ 2CD72948268A828400BFC976 /* Locale+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD72947268A828400BFC976 /* Locale+Extensions.swift */; }; 2D11F5E1250FF886005A70E8 /* AttributionStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D11F5E0250FF886005A70E8 /* AttributionStrings.swift */; }; 2D1C3F3926B9D8B800112626 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C3F3826B9D8B800112626 /* MockBundle.swift */; }; + 2D1E789926FE216800760949 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1E788926FE215500760949 /* SceneDelegate.swift */; }; 2D22BF6526F3CB31001AE2F9 /* FatalErrorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6426F3CB31001AE2F9 /* FatalErrorUtil.swift */; }; 2D22BF6826F3CC6D001AE2F9 /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */; }; - 2D3BFAD126DEA45C00370B11 /* MockSKProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSKProduct.swift */; }; + 2D294E5C26DECFD500B8FE4F /* StoreKit2TransactionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D294E5B26DECFD500B8FE4F /* StoreKit2TransactionListener.swift */; }; + 2D34D9D227481D9B00C05DB6 /* TrialOrIntroPriceEligibilityCheckerSK2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D127481D9B00C05DB6 /* TrialOrIntroPriceEligibilityCheckerSK2Tests.swift */; }; + 2D3BFAD126DEA45C00370B11 /* MockSK1Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */; }; 2D3BFAD226DEA46600370B11 /* MockProductsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35B08709090FBBFB16EBD /* MockProductsRequest.swift */; }; 2D3BFAD326DEA47100370B11 /* MockSKDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E724F6F61B005BC22D /* MockSKDiscount.swift */; }; 2D3BFAD426DEA49200370B11 /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */; }; @@ -24,12 +27,44 @@ 2D4D6AF624F7193700B656BE /* verifyReceiptSample1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2DDE559A24C8B5E300DCB087 /* verifyReceiptSample1.txt */; }; 2D4D6AF724F7193700B656BE /* base64encodedreceiptsample1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2DDE559B24C8B5E300DCB087 /* base64encodedreceiptsample1.txt */; }; 2D4E926526990AB1000E10B0 /* StoreKitWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4E926426990AB1000E10B0 /* StoreKitWrapper.swift */; }; + 2D735F7D26EFF189004E82A7 /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2DE20B7926408921004C597D /* Configuration.storekit */; }; + 2D735F7E26EFF198004E82A7 /* UnitTestsConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2D43017726EBFD7100BAB891 /* UnitTestsConfiguration.storekit */; }; + 2D803F6326F144830069D717 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 2D803F6226F144830069D717 /* Nimble */; }; + 2D803F6626F144BF0069D717 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 2D803F6526F144BF0069D717 /* Nimble */; }; + 2D803F6826F144C40069D717 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 2D803F6726F144C40069D717 /* Nimble */; }; + 2D803F6926F149E70069D717 /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DC5621624EC63420031F69B /* RevenueCat.framework */; }; + 2D90F8B126FD1E52009B9142 /* PurchasesOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8FC8AB26E01AE70049A85C /* PurchasesOrchestratorTests.swift */; }; + 2D90F8B326FD2082009B9142 /* MockProductsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C9439E087F63ECC4F59 /* MockProductsManager.swift */; }; + 2D90F8B426FD208B009B9142 /* MockStoreKitWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515326D44B0A00BD2BD7 /* MockStoreKitWrapper.swift */; }; + 2D90F8B526FD2093009B9142 /* MockSystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514626D44A0D00BD2BD7 /* MockSystemInfo.swift */; }; + 2D90F8B626FD2099009B9142 /* MockSubscriberAttributesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514226D449C100BD2BD7 /* MockSubscriberAttributesManager.swift */; }; + 2D90F8B826FD20AA009B9142 /* MockReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515126D44AF000BD2BD7 /* MockReceiptFetcher.swift */; }; + 2D90F8BB26FD20BD009B9142 /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513E26D4496000BD2BD7 /* MockIdentityManager.swift */; }; + 2D90F8BC26FD20C2009B9142 /* MockReceiptParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B13440508B46C9A530 /* MockReceiptParser.swift */; }; + 2D90F8BF26FD20D6009B9142 /* MockAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515926D44B6200BD2BD7 /* MockAttributionFetcher.swift */; }; + 2D90F8C026FD20DF009B9142 /* MockAttributionDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515F26D44BB600BD2BD7 /* MockAttributionDataMigrator.swift */; }; + 2D90F8C126FD20F2009B9142 /* MockHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514C26D44A8600BD2BD7 /* MockHTTPClient.swift */; }; + 2D90F8C226FD20F7009B9142 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; }; + 2D90F8C326FD214F009B9142 /* MockAttributionTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514426D449E600BD2BD7 /* MockAttributionTypeFactory.swift */; }; + 2D90F8C526FD216A009B9142 /* MockSKDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E724F6F61B005BC22D /* MockSKDiscount.swift */; }; + 2D90F8C726FD221D009B9142 /* MockASN1ContainerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351EB3689AF304E5B1031 /* MockASN1ContainerBuilder.swift */; }; + 2D90F8C826FD2225009B9142 /* MockAppleReceiptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C1554F296F7F1317747 /* MockAppleReceiptBuilder.swift */; }; + 2D90F8CA26FD257A009B9142 /* MockStoreKit2TransactionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D90F8C926FD257A009B9142 /* MockStoreKit2TransactionListener.swift */; }; + 2D90F8CC26FD2BA1009B9142 /* StoreKitConfigTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D90F8CB26FD2BA1009B9142 /* StoreKitConfigTestCase.swift */; }; + 2D971CC12744364C0093F35F /* SKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D971CC02744364C0093F35F /* SKError+Extensions.swift */; }; 2D991ACA268BA56900085481 /* StoreKitRequestFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D991AC9268BA56900085481 /* StoreKitRequestFetcher.swift */; }; + 2D9C5EC826F280510057FC45 /* ProductDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69384426DFF93300FCDBC0 /* ProductDetailsTests.swift */; }; + 2D9C5ECA26F2805C0057FC45 /* ProductsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9C5EC926F2805C0057FC45 /* ProductsManagerTests.swift */; }; + 2D9C5ED126F2816F0057FC45 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 2D9C5ED026F2816F0057FC45 /* OHHTTPStubs */; }; + 2D9C5ED326F2816F0057FC45 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2D9C5ED226F2816F0057FC45 /* OHHTTPStubsSwift */; }; + 2D9C5ED526F281750057FC45 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 2D9C5ED426F281750057FC45 /* OHHTTPStubs */; }; + 2D9C5ED726F281750057FC45 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2D9C5ED626F281750057FC45 /* OHHTTPStubsSwift */; }; 2D9C7BB326D838FC006838BE /* UIApplication+RCExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9C7BB226D838FC006838BE /* UIApplication+RCExtensions.swift */; }; 2D9F4A5526C30CA800B07B43 /* PurchasesOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */; }; 2DA85A8A26DEA7DC00F1136D /* MockProductsRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */; }; 2DA85A8B26DEA7DD00F1136D /* MockProductsRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */; }; 2DA85A8C26DEA7FB00F1136D /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DC5621624EC63420031F69B /* RevenueCat.framework */; }; + 2DAC5F7B26F13D1400C5258F /* UnitTestsConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2D43017726EBFD7100BAB891 /* UnitTestsConfiguration.storekit */; }; 2DC19195255F36D10039389A /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC19194255F36D10039389A /* Logger.swift */; }; 2DC5622E24EC636C0031F69B /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E355CBB3F3A31A32687B14 /* Transaction.swift */; }; 2DC5623024EC63730031F69B /* OperationDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDA3E4624DB0B5400EDFE5B /* OperationDispatcher.swift */; }; @@ -64,18 +99,23 @@ 2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B13440508B46C9A530 /* MockReceiptParser.swift */; }; 2DDF41E224F6F527005BC22D /* MockProductsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35B08709090FBBFB16EBD /* MockProductsRequest.swift */; }; 2DDF41E324F6F527005BC22D /* MockASN1ContainerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351EB3689AF304E5B1031 /* MockASN1ContainerBuilder.swift */; }; - 2DDF41E624F6F5DC005BC22D /* MockSKProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSKProduct.swift */; }; + 2DDF41E624F6F5DC005BC22D /* MockSK1Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */; }; 2DDF41E824F6F61B005BC22D /* MockSKDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E724F6F61B005BC22D /* MockSKDiscount.swift */; }; 2DDF41EA24F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */; }; - 2DE20B6F264087FB004C597D /* StoreKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE20B6E264087FB004C597D /* StoreKitTests.swift */; }; + 2DE20B6F264087FB004C597D /* BackendIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE20B6E264087FB004C597D /* BackendIntegrationTests.swift */; }; 2DE20B7626408807004C597D /* StoreKitTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DE20B7526408806004C597D /* StoreKitTest.framework */; }; - 2DE20B7A26408921004C597D /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2DE20B7926408921004C597D /* Configuration.storekit */; }; - 2DE20B8226409EB7004C597D /* StoreKitTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE20B8126409EB7004C597D /* StoreKitTestAppApp.swift */; }; + 2DE20B8226409EB7004C597D /* BackendIntegrationTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE20B8126409EB7004C597D /* BackendIntegrationTestApp.swift */; }; 2DE20B8426409EB7004C597D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE20B8326409EB7004C597D /* ContentView.swift */; }; 2DE20B8626409EB8004C597D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DE20B8526409EB8004C597D /* Assets.xcassets */; }; 2DE20B8926409EB8004C597D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DE20B8826409EB8004C597D /* Preview Assets.xcassets */; }; 2DE20B9226409ECF004C597D /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DE20B9126409ECF004C597D /* StoreKit.framework */; }; 2DE61A84264190830021CEA0 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE61A83264190830021CEA0 /* Constants.swift */; }; + 2DEAC2DD26EFE46E006914ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DEAC2DC26EFE46E006914ED /* AppDelegate.swift */; }; + 2DEAC2E126EFE46E006914ED /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DEAC2E026EFE46E006914ED /* ViewController.swift */; }; + 2DEAC2E426EFE46E006914ED /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DEAC2E226EFE46E006914ED /* Main.storyboard */; }; + 2DEAC2E626EFE470006914ED /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DEAC2E526EFE470006914ED /* Assets.xcassets */; }; + 2DEAC2E926EFE470006914ED /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2DEAC2E726EFE470006914ED /* LaunchScreen.storyboard */; }; + 2DFF6C56270CA28800ECAFAB /* MockRequestFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */; }; 351B513B26D448F400BD2BD7 /* AttributionPosterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354FE32DD3EA3FF3ECD0A /* AttributionPosterTests.swift */; }; 351B513D26D4491E00BD2BD7 /* MockDeviceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513C26D4491E00BD2BD7 /* MockDeviceCache.swift */; }; 351B513F26D4496000BD2BD7 /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513E26D4496000BD2BD7 /* MockIdentityManager.swift */; }; @@ -111,9 +151,8 @@ 351B51A426D450BC00BD2BD7 /* NSError+RCExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353AF2CAD3CEDE6D9B368 /* NSError+RCExtensionsTests.swift */; }; 351B51A526D450BC00BD2BD7 /* NSDate+RCExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3508EC20EEBAB4EAC4C82 /* NSDate+RCExtensionsTests.swift */; }; 351B51A726D450D400BD2BD7 /* SystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35EEE7783629CDE41B70C /* SystemInfoTests.swift */; }; - 351B51B526D450E800BD2BD7 /* ProductsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351F0E21361EAEC078A0D /* ProductsManagerTests.swift */; }; + 351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351F0E21361EAEC078A0D /* ProductsFetcherSK1Tests.swift */; }; 351B51B626D450E800BD2BD7 /* ReceiptFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84458826B9CD270033B5A3 /* ReceiptFetcherTests.swift */; }; - 351B51B726D450E800BD2BD7 /* RCIntroEligibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD02D5624AD0B0500419CD9 /* RCIntroEligibilityTests.swift */; }; 351B51B826D450E800BD2BD7 /* StoreKitWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E352F86A0A8EB05BAD77C4 /* StoreKitWrapperTests.swift */; }; 351B51B926D450E800BD2BD7 /* TransactionsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3597021124BF6AAC0010506E /* TransactionsFactoryTests.swift */; }; 351B51BA26D450E800BD2BD7 /* ProductInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3583675928C01D92E3166 /* ProductInfoExtensions.swift */; }; @@ -128,9 +167,20 @@ 351B51C326D450F200BD2BD7 /* InMemoryCachedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35E3250FBBB03D92E06EC /* InMemoryCachedObjectTests.swift */; }; 35272E1B26D0029300F22C3B /* DeviceCacheSubscriberAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C4A795A0F056381A1B3 /* DeviceCacheSubscriberAttributesTests.swift */; }; 35272E2226D0048D00F22C3B /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353CBE9CF2572A72A347F /* HTTPClientTests.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 */; }; + 3543913C26F9101600E669DF /* MockOperationDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514A26D44A4A00BD2BD7 /* MockOperationDispatcher.swift */; }; + 3543914126F911CC00E669DF /* MockDeviceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B513C26D4491E00BD2BD7 /* MockDeviceCache.swift */; }; + 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 */; }; 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 */; }; + 359E3ABF26FBE8CD0094617C /* PartialMockProductsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359E3ABE26FBE8CD0094617C /* PartialMockProductsManager.swift */; }; + 359E8E3F26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359E8E3E26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift */; }; + 35B745A82711001A00458D46 /* MockManageSubscriptionsModalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift */; }; 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */; }; 35D832CD262A5B7500E60AC5 /* ETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D832CC262A5B7500E60AC5 /* ETagManager.swift */; }; 35D832D2262E56DB00E60AC5 /* HTTPStatusCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D832D1262E56DB00E60AC5 /* HTTPStatusCodes.swift */; }; @@ -138,20 +188,26 @@ 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 */; }; + 35E840CC270FB70D00899AE2 /* ManageSubscriptionsModalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C5270FB47C00899AE2 /* ManageSubscriptionsModalHelper.swift */; }; + 35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift */; }; 35F6FD62267426D600ABCB53 /* ETagAndResponseWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.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 */; }; 35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB526A9B8030051DF03 /* AttributionDataMigrator.swift */; }; + 35F8B8FA26E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8B8F926E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift */; }; 37E350C67712B9E054FEF297 /* AttributionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35CD16BB73BB091E64D9A /* AttributionData.swift */; }; 37E3529267833EA8ED9F06BE /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353FD94A8FD5CD8796530 /* DateFormatter+Extensions.swift */; }; 37E352973B0901E3CAA717E1 /* DateFormatter+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */; }; 37E3578711F5FDD5DC6458A8 /* AttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */; }; 37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3507939634ED5A9280544 /* Strings.swift */; }; - 57EAE527274324C60060EB74 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE526274324C60060EB74 /* Lock.swift */; }; + 42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */; }; 5791CE80273F26A000E50C4B /* base64encoded_sandboxReceipt.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5791CE7F273F26A000E50C4B /* base64encoded_sandboxReceipt.txt */; }; - 57EAE52D274468900060EB74 /* RawDataContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE52C274468900060EB74 /* RawDataContainer.swift */; }; + 57EAE527274324C60060EB74 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE526274324C60060EB74 /* Lock.swift */; }; 57EAE52B274332830060EB74 /* Obsoletions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE52A274332830060EB74 /* Obsoletions.swift */; }; + 57EAE52D274468900060EB74 /* RawDataContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE52C274468900060EB74 /* RawDataContainer.swift */; }; + 6E38843A0CAFD551013D0A3F /* ProductDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* ProductDetails.swift */; }; + 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; 80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; }; 9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; }; 9A65E03625918B0500DE00B0 /* ConfigureStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */; }; @@ -163,7 +219,14 @@ 9A65E0A52591A23500DE00B0 /* PurchaseStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E0A42591A23500DE00B0 /* PurchaseStrings.swift */; }; 9A65E0AA2591A23800DE00B0 /* RestoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E0A92591A23800DE00B0 /* RestoreStrings.swift */; }; A525BF4B26C320D100C354C4 /* SubscriberAttributesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A525BF4A26C320D100C354C4 /* SubscriberAttributesManager.swift */; }; + A55D08302722368600D919E0 /* SK2BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55D082F2722368600D919E0 /* SK2BeginRefundRequestHelper.swift */; }; + A55D083427235DED00D919E0 /* BeginRefundRequestHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F587271E076800246E0C /* BeginRefundRequestHelperTests.swift */; }; + A55D083527235E2800D919E0 /* ManageSubscriptionsModalHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C9270FB52800899AE2 /* ManageSubscriptionsModalHelperTests.swift */; }; + A55D083627236F0200D919E0 /* MockSK2BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55D08312722471200D919E0 /* MockSK2BeginRefundRequestHelper.swift */; }; + A563F586271E072B00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; + A563F589271E1DAD00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; A56F9AB126990E9200AFC48F /* CustomerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56F9AB026990E9200AFC48F /* CustomerInfo.swift */; }; + A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */; }; B300E4BF26D436F900B22262 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; }; B300E4C026D4371200B22262 /* SKPaymentTransactionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492526B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift */; }; B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */; }; @@ -203,10 +266,6 @@ B3B5FBC1269E17CE00104A0C /* DeviceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B5FBC0269E17CE00104A0C /* DeviceCache.swift */; }; B3C1AEFA268FF4DB0013D50D /* ProductInfoEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C1AEF9268FF4DB0013D50D /* ProductInfoEnums.swift */; }; B3C4AAD526B8911300E1B3C8 /* Backend.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C4AAD426B8911300E1B3C8 /* Backend.swift */; }; - B3D5CFC42672827D0056FA67 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = B3D5CFC32672827D0056FA67 /* Nimble */; }; - B3D5CFC62672827D0056FA67 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B3D5CFC52672827D0056FA67 /* OHHTTPStubs */; }; - B3D5CFC82672827D0056FA67 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B3D5CFC72672827D0056FA67 /* OHHTTPStubsSwift */; }; - B3D5CFCA267282860056FA67 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = B3D5CFC9267282860056FA67 /* Nimble */; }; B3DDB55926854865008CCF23 /* PurchaseOwnershipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DDB55826854865008CCF23 /* PurchaseOwnershipType.swift */; }; B3E26A4A26BE0A8E003ACCF3 /* Error+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E26A4926BE0A8E003ACCF3 /* Error+Extensions.swift */; }; B3F8418F26F3A93400E560FB /* ErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F8418E26F3A93400E560FB /* ErrorCodeTests.swift */; }; @@ -233,6 +292,27 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 2D803F6A26F150190069D717 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2DC5621524EC63420031F69B; + remoteInfo = RevenueCat; + }; + 2D803F6C26F150200069D717 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2DC5621524EC63420031F69B; + remoteInfo = RevenueCat; + }; + 2DAC5F7626F13C9800C5258F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2DEAC2D926EFE46E006914ED; + remoteInfo = UnitTestsHostApp; + }; 2DC5622024EC63430031F69B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; @@ -247,6 +327,13 @@ remoteGlobalIDString = 2DE20B7E26409EB7004C597D; remoteInfo = StoreKitTestApp; }; + 2DFF6C54270CA11400ECAFAB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 352629F51F7C4B9100C04F2C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2DEAC2D926EFE46E006914ED; + remoteInfo = UnitTestsHostApp; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -256,17 +343,29 @@ 2CD72947268A828400BFC976 /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = ""; }; 2D11F5E0250FF886005A70E8 /* AttributionStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionStrings.swift; sourceTree = ""; }; 2D1C3F3826B9D8B800112626 /* MockBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBundle.swift; sourceTree = ""; }; + 2D1E788926FE215500760949 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 2D22BF6426F3CB31001AE2F9 /* FatalErrorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorUtil.swift; sourceTree = ""; }; 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Extensions.swift"; sourceTree = ""; }; + 2D294E5B26DECFD500B8FE4F /* StoreKit2TransactionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionListener.swift; sourceTree = ""; }; + 2D34D9D127481D9B00C05DB6 /* TrialOrIntroPriceEligibilityCheckerSK2Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK2Tests.swift; sourceTree = ""; }; + 2D43017726EBFD7100BAB891 /* UnitTestsConfiguration.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = UnitTestsConfiguration.storekit; sourceTree = ""; }; 2D4E926426990AB1000E10B0 /* StoreKitWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitWrapper.swift; sourceTree = ""; }; + 2D545DBF26F8F96D0044BAC9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; 2D5BB46A24C8E8ED00E27537 /* ReceiptParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptParser.swift; sourceTree = ""; }; + 2D69384426DFF93300FCDBC0 /* ProductDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsTests.swift; sourceTree = ""; }; 2D84458826B9CD270033B5A3 /* ReceiptFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcherTests.swift; sourceTree = ""; }; 2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberAttributeTests.swift; sourceTree = ""; }; 2D8F622224D30F9D00F993AA /* ReceiptParsingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptParsingError.swift; sourceTree = ""; }; + 2D8FC8AB26E01AE70049A85C /* PurchasesOrchestratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesOrchestratorTests.swift; sourceTree = ""; }; + 2D90F8C926FD257A009B9142 /* MockStoreKit2TransactionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2TransactionListener.swift; sourceTree = ""; }; + 2D90F8CB26FD2BA1009B9142 /* StoreKitConfigTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitConfigTestCase.swift; sourceTree = ""; }; + 2D971CC02744364C0093F35F /* SKError+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKError+Extensions.swift"; sourceTree = ""; }; 2D97458E24BDFCEF006245E9 /* IntroEligibilityCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroEligibilityCalculator.swift; sourceTree = ""; }; 2D991AC9268BA56900085481 /* StoreKitRequestFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitRequestFetcher.swift; sourceTree = ""; }; + 2D9C5EC926F2805C0057FC45 /* ProductsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsManagerTests.swift; sourceTree = ""; }; 2D9C7BB226D838FC006838BE /* UIApplication+RCExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+RCExtensions.swift"; sourceTree = ""; }; 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesOrchestrator.swift; sourceTree = ""; }; + 2DAC5F7226F13C9800C5258F /* StoreKitUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StoreKitUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2DBB3E10269C9B8700BBF431 /* SwiftStyleGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftStyleGuide.swift; sourceTree = ""; }; 2DC19194255F36D10039389A /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 2DC5621624EC63420031F69B /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -274,7 +373,6 @@ 2DC5621924EC63430031F69B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2DC5621E24EC63430031F69B /* RevenueCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RevenueCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2DC5622524EC63430031F69B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2DD02D5624AD0B0500419CD9 /* RCIntroEligibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCIntroEligibilityTests.swift; sourceTree = ""; }; 2DD269162522A20A006AC4BC /* DictionaryExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtensionsTests.swift; sourceTree = ""; }; 2DDA3E4624DB0B5400EDFE5B /* OperationDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDispatcher.swift; sourceTree = ""; }; 2DDE559A24C8B5E300DCB087 /* verifyReceiptSample1.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = verifyReceiptSample1.txt; sourceTree = ""; }; @@ -296,22 +394,29 @@ 2DDF41C424F6F4C3005BC22D /* ASN1ContainerBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ASN1ContainerBuilderTests.swift; sourceTree = ""; }; 2DDF41C524F6F4C3005BC22D /* InAppPurchaseBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppPurchaseBuilderTests.swift; sourceTree = ""; }; 2DDF41C724F6F4C3005BC22D /* ReceiptParsing+TestsWithRealReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReceiptParsing+TestsWithRealReceipts.swift"; sourceTree = ""; }; - 2DDF41E524F6F5DC005BC22D /* MockSKProduct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSKProduct.swift; sourceTree = ""; }; + 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSK1Product.swift; sourceTree = ""; }; 2DDF41E724F6F61B005BC22D /* MockSKDiscount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSKDiscount.swift; sourceTree = ""; }; 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKProductSubscriptionDurationExtensions.swift; sourceTree = ""; }; - 2DE20B6C264087FB004C597D /* StoreKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StoreKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2DE20B6E264087FB004C597D /* StoreKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitTests.swift; sourceTree = ""; }; + 2DE20B6C264087FB004C597D /* BackendIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BackendIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2DE20B6E264087FB004C597D /* BackendIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendIntegrationTests.swift; sourceTree = ""; }; 2DE20B70264087FB004C597D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2DE20B7526408806004C597D /* StoreKitTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKitTest.framework; path = Developer/Library/Frameworks/StoreKitTest.framework; sourceTree = SDKROOT; }; 2DE20B7926408921004C597D /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Configuration.storekit; path = IntegrationTests/PurchaseTester/Configuration.storekit; sourceTree = SOURCE_ROOT; }; - 2DE20B7F26409EB7004C597D /* StoreKitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StoreKitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 2DE20B8126409EB7004C597D /* StoreKitTestAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitTestAppApp.swift; sourceTree = ""; }; + 2DE20B7F26409EB7004C597D /* BackendIntegrationTestsHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BackendIntegrationTestsHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2DE20B8126409EB7004C597D /* BackendIntegrationTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendIntegrationTestApp.swift; sourceTree = ""; }; 2DE20B8326409EB7004C597D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2DE20B8526409EB8004C597D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2DE20B8826409EB8004C597D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 2DE20B8A26409EB8004C597D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2DE20B9126409ECF004C597D /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk/System/iOSSupport/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; 2DE61A83264190830021CEA0 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 2DEAC2DA26EFE46E006914ED /* UnitTestsHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestsHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2DEAC2DC26EFE46E006914ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2DEAC2E026EFE46E006914ED /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 2DEAC2E326EFE46E006914ED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 2DEAC2E526EFE470006914ED /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2DEAC2E826EFE470006914ED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2DEAC2EA26EFE470006914ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2DEC0CFB24A2A1B100B0E5BB /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; 33FFC8744F2BAE7BD8889A4C /* Pods_RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 350A1B84226E3E8700CCA10F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -344,17 +449,25 @@ 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; }; 3597020F24BF6A710010506E /* TransactionsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsFactory.swift; sourceTree = ""; }; 3597021124BF6AAC0010506E /* TransactionsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsFactoryTests.swift; sourceTree = ""; }; + 359E3ABE26FBE8CD0094617C /* PartialMockProductsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialMockProductsManager.swift; sourceTree = ""; }; + 359E8E3E26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityChecker.swift; sourceTree = ""; }; 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorUtils.swift; sourceTree = ""; }; 35D832CC262A5B7500E60AC5 /* ETagManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETagManager.swift; sourceTree = ""; }; 35D832D1262E56DB00E60AC5 /* HTTPStatusCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCodes.swift; sourceTree = ""; }; 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 = ""; }; + 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK1Tests.swift; sourceTree = ""; }; + 35E840C5270FB47C00899AE2 /* ManageSubscriptionsModalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsModalHelper.swift; sourceTree = ""; }; + 35E840C9270FB52800899AE2 /* ManageSubscriptionsModalHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsModalHelperTests.swift; sourceTree = ""; }; + 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManageSubscriptionsModalHelper.swift; sourceTree = ""; }; 35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETagAndResponseWrapper.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 = ""; }; 35F82BB526A9B8030051DF03 /* AttributionDataMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionDataMigrator.swift; sourceTree = ""; }; + 35F8B8F926E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTrialOrIntroPriceEligibilityChecker.swift; sourceTree = ""; }; + 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsFetcherSK2.swift; sourceTree = ""; }; 37E350420D54B99BB39448E0 /* AttributionTypeFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionTypeFactoryTests.swift; sourceTree = ""; }; 37E3507939634ED5A9280544 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 37E3508E52201122137D4B4A /* PurchasesSubscriberAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchasesSubscriberAttributesTests.swift; sourceTree = ""; }; @@ -363,7 +476,7 @@ 37E350E57B0A393455A72B40 /* ProductInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductInfoTests.swift; sourceTree = ""; }; 37E351D0EBC4698E1D3585A6 /* ReceiptParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiptParserTests.swift; sourceTree = ""; }; 37E351EB3689AF304E5B1031 /* MockASN1ContainerBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockASN1ContainerBuilder.swift; sourceTree = ""; }; - 37E351F0E21361EAEC078A0D /* ProductsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsManagerTests.swift; sourceTree = ""; }; + 37E351F0E21361EAEC078A0D /* ProductsFetcherSK1Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsFetcherSK1Tests.swift; sourceTree = ""; }; 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionFetcher.swift; sourceTree = ""; }; 37E35294EBC1E5A879C95540 /* IdentityManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityManagerTests.swift; sourceTree = ""; }; 37E352F86A0A8EB05BAD77C4 /* StoreKitWrapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreKitWrapperTests.swift; sourceTree = ""; }; @@ -402,10 +515,10 @@ 37E35EEE7783629CDE41B70C /* SystemInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemInfoTests.swift; sourceTree = ""; }; 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProductsRequestFactory.swift; sourceTree = ""; }; 37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+ExtensionsTests.swift"; sourceTree = ""; }; - 57EAE526274324C60060EB74 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; 5791CE7F273F26A000E50C4B /* base64encoded_sandboxReceipt.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = base64encoded_sandboxReceipt.txt; sourceTree = ""; }; - 57EAE52C274468900060EB74 /* RawDataContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawDataContainer.swift; sourceTree = ""; }; + 57EAE526274324C60060EB74 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; 57EAE52A274332830060EB74 /* Obsoletions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Obsoletions.swift; sourceTree = ""; }; + 57EAE52C274468900060EB74 /* RawDataContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawDataContainer.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; }; 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogIntent.swift; sourceTree = ""; }; @@ -418,7 +531,13 @@ 9A65E0A42591A23500DE00B0 /* PurchaseStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseStrings.swift; sourceTree = ""; }; 9A65E0A92591A23800DE00B0 /* RestoreStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreStrings.swift; sourceTree = ""; }; A525BF4A26C320D100C354C4 /* SubscriberAttributesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriberAttributesManager.swift; sourceTree = ""; }; + A55C3D002734ADB40016E9FD /* SKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKError+Extensions.swift"; sourceTree = ""; }; + A55D082F2722368600D919E0 /* SK2BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SK2BeginRefundRequestHelper.swift; sourceTree = ""; }; + A55D08312722471200D919E0 /* MockSK2BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSK2BeginRefundRequestHelper.swift; sourceTree = ""; }; + A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBeginRefundRequestHelper.swift; sourceTree = ""; }; + A563F587271E076800246E0C /* BeginRefundRequestHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelperTests.swift; sourceTree = ""; }; A56F9AB026990E9200AFC48F /* CustomerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfo.swift; sourceTree = ""; }; + A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelper.swift; sourceTree = ""; }; B302206927271BCB008F1A0D /* Decoder+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decoder+Extensions.swift"; sourceTree = ""; }; B302206B2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnexpectedBackendResponseSubErrorCode.swift; sourceTree = ""; }; B302206D2728B798008F1A0D /* BackendErrorStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendErrorStrings.swift; sourceTree = ""; }; @@ -456,6 +575,7 @@ B3DF6A4F269524080030D57C /* IntroEligibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroEligibility.swift; sourceTree = ""; }; B3E26A4926BE0A8E003ACCF3 /* Error+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Extensions.swift"; sourceTree = ""; }; B3F8418E26F3A93400E560FB /* ErrorCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCodeTests.swift; sourceTree = ""; }; + EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsFetcherSK1.swift; sourceTree = ""; }; F5714EA426D6C24D00635477 /* JSONDecoder+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Extensions.swift"; sourceTree = ""; }; F5714EA726D7A83A00635477 /* Store+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Store+Extensions.swift"; sourceTree = ""; }; F5714EA926D7A85D00635477 /* PeriodType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PeriodType+Extensions.swift"; sourceTree = ""; }; @@ -478,9 +598,21 @@ F5BE447C269E4ADB00254A30 /* ASIdManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASIdManagerProxy.swift; sourceTree = ""; }; F5C0196826E880800005D61E /* StoreKitStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitStrings.swift; sourceTree = ""; }; F5E4383726B8530700841CC8 /* SKPaymentTransaction+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKPaymentTransaction+Extensions.swift"; sourceTree = ""; }; + FECF627761D375C8431EB866 /* ProductDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetails.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2DAC5F6F26F13C9800C5258F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D803F6926F149E70069D717 /* RevenueCat.framework in Frameworks */, + 2D9C5ED726F281750057FC45 /* OHHTTPStubsSwift in Frameworks */, + 2D9C5ED526F281750057FC45 /* OHHTTPStubs in Frameworks */, + 2D803F6826F144C40069D717 /* Nimble in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2DC5621324EC63420031F69B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -493,9 +625,9 @@ buildActionMask = 2147483647; files = ( B36824BE268FBC5B00957E4C /* XCTest.framework in Frameworks */, - B3D5CFC82672827D0056FA67 /* OHHTTPStubsSwift in Frameworks */, - B3D5CFC62672827D0056FA67 /* OHHTTPStubs in Frameworks */, - B3D5CFC42672827D0056FA67 /* Nimble in Frameworks */, + 2D9C5ED326F2816F0057FC45 /* OHHTTPStubsSwift in Frameworks */, + 2D9C5ED126F2816F0057FC45 /* OHHTTPStubs in Frameworks */, + 2D803F6326F144830069D717 /* Nimble in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -503,8 +635,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2D803F6626F144BF0069D717 /* Nimble in Frameworks */, 2DA85A8C26DEA7FB00F1136D /* RevenueCat.framework in Frameworks */, - B3D5CFCA267282860056FA67 /* Nimble in Frameworks */, 2DE20B7626408807004C597D /* StoreKitTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -520,6 +652,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 006E105DDA8D0D0FA32D690C /* StoreKit2 */ = { + isa = PBXGroup; + children = ( + 2D294E5B26DECFD500B8FE4F /* StoreKit2TransactionListener.swift */, + 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */, + A55D082F2722368600D919E0 /* SK2BeginRefundRequestHelper.swift */, + ); + path = StoreKit2; + sourceTree = ""; + }; 2CD72940268A820E00BFC976 /* FoundationExtensions */ = { isa = PBXGroup; children = ( @@ -579,9 +721,44 @@ path = LocalReceiptParsing; sourceTree = ""; }; + 2D1DB44A26EA61AB00DDE736 /* StoreKit1 */ = { + isa = PBXGroup; + children = ( + EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */, + 2D991AC9268BA56900085481 /* StoreKitRequestFetcher.swift */, + 2D4E926426990AB1000E10B0 /* StoreKitWrapper.swift */, + F5E4383626B852D700841CC8 /* StoreKitExtensions */, + ); + path = StoreKit1; + sourceTree = ""; + }; + 2D971CBF274432B00093F35F /* Recovered References */ = { + isa = PBXGroup; + children = ( + A55C3D002734ADB40016E9FD /* SKError+Extensions.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + 2DAC5F7326F13C9800C5258F /* StoreKitUnitTests */ = { + isa = PBXGroup; + children = ( + 359E3ABD26FBE8AB0094617C /* Mocks */, + 2D8FC8AB26E01AE70049A85C /* PurchasesOrchestratorTests.swift */, + 2D34D9D127481D9B00C05DB6 /* TrialOrIntroPriceEligibilityCheckerSK2Tests.swift */, + 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */, + 2D69384426DFF93300FCDBC0 /* ProductDetailsTests.swift */, + 2D9C5EC926F2805C0057FC45 /* ProductsManagerTests.swift */, + 2D43017726EBFD7100BAB891 /* UnitTestsConfiguration.storekit */, + 2D90F8CB26FD2BA1009B9142 /* StoreKitConfigTestCase.swift */, + ); + path = StoreKitUnitTests; + sourceTree = ""; + }; 2DC5621724EC63420031F69B /* Purchases */ = { isa = PBXGroup; children = ( + 35E840C1270FB45600899AE2 /* Support */, F5BE44412698580200254A30 /* Attribution */, B3B5FBBD269E080A00104A0C /* Caching */, F5714EA626D7A82E00635477 /* CodableExtensions */, @@ -594,7 +771,6 @@ 35D832CB262A5B3400E60AC5 /* Networking */, B3DDB55726854850008CCF23 /* Public */, 354235D524C11138008C84EE /* Purchasing */, - F5E4383626B852D700841CC8 /* StoreKitExtensions */, 354895D0267AE32D001DC5B1 /* SubscriberAttributes */, 2DC5621924EC63430031F69B /* Info.plist */, ); @@ -604,6 +780,7 @@ 2DC5622224EC63430031F69B /* PurchasesTests */ = { isa = PBXGroup; children = ( + 35E840C8270FB51C00899AE2 /* Support */, F5BE444626985E6E00254A30 /* Attribution */, 37E35AE0CDC4C2AA8260FB58 /* Caching */, 35F82BBB26A9BFA60051DF03 /* FoundationExtensions */, @@ -706,60 +883,65 @@ 351B514026D4498F00BD2BD7 /* MockBackend.swift */, 2D1C3F3826B9D8B800112626 /* MockBundle.swift */, 351B514826D44A2F00BD2BD7 /* MockCustomerInfoManager.swift */, + 351B516F26D44E8D00BD2BD7 /* MockDateProvider.swift */, 351B513C26D4491E00BD2BD7 /* MockDeviceCache.swift */, 35D83311262FBD4200E60AC5 /* MockETagManager.swift */, 351B514C26D44A8600BD2BD7 /* MockHTTPClient.swift */, 351B513E26D4496000BD2BD7 /* MockIdentityManager.swift */, 37E355744D64075AA91342DE /* MockInAppPurchaseBuilder.swift */, + 351B517126D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift */, 351B515B26D44B7900BD2BD7 /* MockIntroEligibilityCalculator.swift */, 351B515526D44B2300BD2BD7 /* MockNotificationCenter.swift */, 351B515726D44B3E00BD2BD7 /* MockOfferingsFactory.swift */, F575858E26C0893600C12B97 /* MockOfferingsManager.swift */, 351B514A26D44A4A00BD2BD7 /* MockOperationDispatcher.swift */, + 351B517326D44F4B00BD2BD7 /* MockPaymentDiscount.swift */, + 351B517526D44F7200BD2BD7 /* MockProductDiscount.swift */, 37E35C9439E087F63ECC4F59 /* MockProductsManager.swift */, 37E35B08709090FBBFB16EBD /* MockProductsRequest.swift */, 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */, 351B515D26D44B9900BD2BD7 /* MockPurchasesDelegate.swift */, 351B515126D44AF000BD2BD7 /* MockReceiptFetcher.swift */, 37E354B13440508B46C9A530 /* MockReceiptParser.swift */, + 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */, 2DDF41E724F6F61B005BC22D /* MockSKDiscount.swift */, - 2DDF41E524F6F5DC005BC22D /* MockSKProduct.swift */, + 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */, 351B515326D44B0A00BD2BD7 /* MockStoreKitWrapper.swift */, 351B514226D449C100BD2BD7 /* MockSubscriberAttributesManager.swift */, 351B514626D44A0D00BD2BD7 /* MockSystemInfo.swift */, F591492726B9956C00D32E58 /* MockTransaction.swift */, + 35F8B8F926E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift */, 37E357D16038F07915D7825D /* MockUserDefaults.swift */, 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */, - 351B516F26D44E8D00BD2BD7 /* MockDateProvider.swift */, - 351B517126D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift */, - 351B517326D44F4B00BD2BD7 /* MockPaymentDiscount.swift */, - 351B517526D44F7200BD2BD7 /* MockProductDiscount.swift */, - 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */, + 2D90F8C926FD257A009B9142 /* MockStoreKit2TransactionListener.swift */, + 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift */, + A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */, + A55D08312722471200D919E0 /* MockSK2BeginRefundRequestHelper.swift */, ); path = Mocks; sourceTree = ""; }; - 2DE20B6D264087FB004C597D /* StoreKitTests */ = { + 2DE20B6D264087FB004C597D /* BackendIntegrationTests */ = { isa = PBXGroup; children = ( 2DE20B7926408921004C597D /* Configuration.storekit */, 2DE61A83264190830021CEA0 /* Constants.swift */, 2DE20B70264087FB004C597D /* Info.plist */, - 2DE20B6E264087FB004C597D /* StoreKitTests.swift */, + 2DE20B6E264087FB004C597D /* BackendIntegrationTests.swift */, ); - path = StoreKitTests; + path = BackendIntegrationTests; sourceTree = ""; }; - 2DE20B8026409EB7004C597D /* StoreKitTestApp */ = { + 2DE20B8026409EB7004C597D /* BackendIntegrationTestApp */ = { isa = PBXGroup; children = ( 2DE20B8726409EB8004C597D /* Preview Content */, 2DE20B8526409EB8004C597D /* Assets.xcassets */, 2DE20B8326409EB7004C597D /* ContentView.swift */, 2DE20B8A26409EB8004C597D /* Info.plist */, - 2DE20B8126409EB7004C597D /* StoreKitTestAppApp.swift */, + 2DE20B8126409EB7004C597D /* BackendIntegrationTestApp.swift */, ); - path = StoreKitTestApp; + path = BackendIntegrationTestApp; sourceTree = ""; }; 2DE20B8726409EB8004C597D /* Preview Content */ = { @@ -770,18 +952,36 @@ path = "Preview Content"; sourceTree = ""; }; + 2DEAC2DB26EFE46E006914ED /* UnitTestsHostApp */ = { + isa = PBXGroup; + children = ( + 2D1E788926FE215500760949 /* SceneDelegate.swift */, + 2DEAC2DC26EFE46E006914ED /* AppDelegate.swift */, + 2DEAC2E026EFE46E006914ED /* ViewController.swift */, + 2DEAC2E226EFE46E006914ED /* Main.storyboard */, + 2DEAC2E526EFE470006914ED /* Assets.xcassets */, + 2DEAC2E726EFE470006914ED /* LaunchScreen.storyboard */, + 2DEAC2EA26EFE470006914ED /* Info.plist */, + ); + path = UnitTestsHostApp; + sourceTree = ""; + }; 352629F41F7C4B9100C04F2C = { isa = PBXGroup; children = ( B3B4E76026E036E700C200D3 /* RevenueCat.xctestplan */, + 2D545DBF26F8F96D0044BAC9 /* UnitTests.xctestplan */, 2DC5621724EC63420031F69B /* Purchases */, 2DC5622224EC63430031F69B /* PurchasesTests */, - 2DE20B6D264087FB004C597D /* StoreKitTests */, - 2DE20B8026409EB7004C597D /* StoreKitTestApp */, + 2DAC5F7326F13C9800C5258F /* StoreKitUnitTests */, + 2DE20B6D264087FB004C597D /* BackendIntegrationTests */, + 2DE20B8026409EB7004C597D /* BackendIntegrationTestApp */, + 2DEAC2DB26EFE46E006914ED /* UnitTestsHostApp */, 352629FF1F7C4B9100C04F2C /* Products */, 3530C18722653E8F00D6DF52 /* Frameworks */, 2DBB3E10269C9B8700BBF431 /* SwiftStyleGuide.swift */, 2DEC0CFB24A2A1B100B0E5BB /* Package.swift */, + 2D971CBF274432B00093F35F /* Recovered References */, ); sourceTree = ""; }; @@ -790,8 +990,10 @@ children = ( 2DC5621624EC63420031F69B /* RevenueCat.framework */, 2DC5621E24EC63430031F69B /* RevenueCatTests.xctest */, - 2DE20B6C264087FB004C597D /* StoreKitTests.xctest */, - 2DE20B7F26409EB7004C597D /* StoreKitTestApp.app */, + 2DE20B6C264087FB004C597D /* BackendIntegrationTests.xctest */, + 2DE20B7F26409EB7004C597D /* BackendIntegrationTestsHostApp.app */, + 2DEAC2DA26EFE46E006914ED /* UnitTestsHostApp.app */, + 2DAC5F7226F13C9800C5258F /* StoreKitUnitTests.xctest */, ); name = Products; sourceTree = ""; @@ -824,6 +1026,8 @@ 354235D524C11138008C84EE /* Purchasing */ = { isa = PBXGroup; children = ( + 2D1DB44A26EA61AB00DDE736 /* StoreKit1 */, + 006E105DDA8D0D0FA32D690C /* StoreKit2 */, 2D97458E24BDFCEF006245E9 /* IntroEligibilityCalculator.swift */, 35549322269E298B005F9AE9 /* OfferingsFactory.swift */, F575858C26C088FE00C12B97 /* OfferingsManager.swift */, @@ -836,9 +1040,9 @@ 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */, 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */, F5BE423F26962ACF00254A30 /* ReceiptRefreshPolicy.swift */, - 2D991AC9268BA56900085481 /* StoreKitRequestFetcher.swift */, - 2D4E926426990AB1000E10B0 /* StoreKitWrapper.swift */, 3597020F24BF6A710010506E /* TransactionsFactory.swift */, + FECF627761D375C8431EB866 /* ProductDetails.swift */, + 359E8E3E26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift */, ); path = Purchasing; sourceTree = ""; @@ -846,7 +1050,6 @@ 354235D624C11160008C84EE /* Purchasing */ = { isa = PBXGroup; children = ( - 37E359D8F304C83184560135 /* CustomerInfoTests.swift */, 37E3582920E16E065502E5FC /* EntitlementInfosTests.swift */, B3F8418E26F3A93400E560FB /* ErrorCodeTests.swift */, 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */, @@ -855,10 +1058,10 @@ 37E3583675928C01D92E3166 /* ProductInfoExtensions.swift */, 37E3548189DA008320B3FC98 /* ProductInfoExtractorTests.swift */, 37E350E57B0A393455A72B40 /* ProductInfoTests.swift */, - 37E351F0E21361EAEC078A0D /* ProductsManagerTests.swift */, B390F5B9271DDC7400B64D65 /* PurchasesDeprecation.swift */, + 37E351F0E21361EAEC078A0D /* ProductsFetcherSK1Tests.swift */, + 37E359D8F304C83184560135 /* CustomerInfoTests.swift */, 37E35DE5707E845DA3FF51BC /* PurchasesTests.swift */, - 2DD02D5624AD0B0500419CD9 /* RCIntroEligibilityTests.swift */, 2D84458826B9CD270033B5A3 /* ReceiptFetcherTests.swift */, F5BE4244269676E200254A30 /* StoreKitRequestFetcherTests.swift */, 37E352F86A0A8EB05BAD77C4 /* StoreKitWrapperTests.swift */, @@ -879,6 +1082,14 @@ path = SubscriberAttributes; sourceTree = ""; }; + 359E3ABD26FBE8AB0094617C /* Mocks */ = { + isa = PBXGroup; + children = ( + 359E3ABE26FBE8CD0094617C /* PartialMockProductsManager.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 35D832CB262A5B3400E60AC5 /* Networking */ = { isa = PBXGroup; children = ( @@ -904,6 +1115,24 @@ path = Networking; sourceTree = ""; }; + 35E840C1270FB45600899AE2 /* Support */ = { + isa = PBXGroup; + children = ( + 35E840C5270FB47C00899AE2 /* ManageSubscriptionsModalHelper.swift */, + A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */, + ); + path = Support; + sourceTree = ""; + }; + 35E840C8270FB51C00899AE2 /* Support */ = { + isa = PBXGroup; + children = ( + 35E840C9270FB52800899AE2 /* ManageSubscriptionsModalHelperTests.swift */, + A563F587271E076800246E0C /* BeginRefundRequestHelperTests.swift */, + ); + path = Support; + sourceTree = ""; + }; 35F82BBB26A9BFA60051DF03 /* FoundationExtensions */ = { isa = PBXGroup; children = ( @@ -981,6 +1210,7 @@ B3022071272B3DDC008F1A0D /* DescribableError.swift */, B3B5FBB5269CED6400104A0C /* ErrorDetails.swift */, 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */, + 2D971CC02744364C0093F35F /* SKError+Extensions.swift */, B302206B2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift */, ); path = "Error Handling"; @@ -1112,6 +1342,30 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 2DAC5F7126F13C9800C5258F /* StoreKitUnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2DAC5F7826F13C9800C5258F /* Build configuration list for PBXNativeTarget "StoreKitUnitTests" */; + buildPhases = ( + 2DAC5F6E26F13C9800C5258F /* Sources */, + 2DAC5F6F26F13C9800C5258F /* Frameworks */, + 2DAC5F7026F13C9800C5258F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2D803F6B26F150190069D717 /* PBXTargetDependency */, + 2DAC5F7726F13C9800C5258F /* PBXTargetDependency */, + ); + name = StoreKitUnitTests; + packageProductDependencies = ( + 2D803F6726F144C40069D717 /* Nimble */, + 2D9C5ED426F281750057FC45 /* OHHTTPStubs */, + 2D9C5ED626F281750057FC45 /* OHHTTPStubsSwift */, + ); + productName = StoreKitUnitTests; + productReference = 2DAC5F7226F13C9800C5258F /* StoreKitUnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 2DC5621524EC63420031F69B /* RevenueCat */ = { isa = PBXNativeTarget; buildConfigurationList = 2DC5622724EC63430031F69B /* Build configuration list for PBXNativeTarget "RevenueCat" */; @@ -1128,6 +1382,8 @@ dependencies = ( ); name = RevenueCat; + packageProductDependencies = ( + ); productName = Purchases; productReference = 2DC5621624EC63420031F69B /* RevenueCat.framework */; productType = "com.apple.product-type.framework"; @@ -1144,20 +1400,21 @@ ); dependencies = ( 2DC5622124EC63430031F69B /* PBXTargetDependency */, + 2DFF6C55270CA11400ECAFAB /* PBXTargetDependency */, ); name = RevenueCatTests; packageProductDependencies = ( - B3D5CFC32672827D0056FA67 /* Nimble */, - B3D5CFC52672827D0056FA67 /* OHHTTPStubs */, - B3D5CFC72672827D0056FA67 /* OHHTTPStubsSwift */, + 2D803F6226F144830069D717 /* Nimble */, + 2D9C5ED026F2816F0057FC45 /* OHHTTPStubs */, + 2D9C5ED226F2816F0057FC45 /* OHHTTPStubsSwift */, ); productName = PurchasesTests; productReference = 2DC5621E24EC63430031F69B /* RevenueCatTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 2DE20B6B264087FB004C597D /* StoreKitTests */ = { + 2DE20B6B264087FB004C597D /* BackendIntegrationTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 2DE20B73264087FB004C597D /* Build configuration list for PBXNativeTarget "StoreKitTests" */; + buildConfigurationList = 2DE20B73264087FB004C597D /* Build configuration list for PBXNativeTarget "BackendIntegrationTests" */; buildPhases = ( 2DE20B68264087FB004C597D /* Sources */, 2DE20B69264087FB004C597D /* Frameworks */, @@ -1166,19 +1423,20 @@ buildRules = ( ); dependencies = ( + 2D803F6D26F150200069D717 /* PBXTargetDependency */, 2DE20B8F26409EC0004C597D /* PBXTargetDependency */, ); - name = StoreKitTests; + name = BackendIntegrationTests; packageProductDependencies = ( - B3D5CFC9267282860056FA67 /* Nimble */, + 2D803F6526F144BF0069D717 /* Nimble */, ); - productName = StoreKitTests; - productReference = 2DE20B6C264087FB004C597D /* StoreKitTests.xctest */; + productName = BackendIntegrationTests; + productReference = 2DE20B6C264087FB004C597D /* BackendIntegrationTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 2DE20B7E26409EB7004C597D /* StoreKitTestApp */ = { + 2DE20B7E26409EB7004C597D /* BackendIntegrationTestsHostApp */ = { isa = PBXNativeTarget; - buildConfigurationList = 2DE20B8B26409EB8004C597D /* Build configuration list for PBXNativeTarget "StoreKitTestApp" */; + buildConfigurationList = 2DE20B8B26409EB8004C597D /* Build configuration list for PBXNativeTarget "BackendIntegrationTestsHostApp" */; buildPhases = ( 2DE20B7B26409EB7004C597D /* Sources */, 2DE20B7C26409EB7004C597D /* Frameworks */, @@ -1188,9 +1446,25 @@ ); dependencies = ( ); - name = StoreKitTestApp; + name = BackendIntegrationTestsHostApp; productName = StoreKitTestApp; - productReference = 2DE20B7F26409EB7004C597D /* StoreKitTestApp.app */; + productReference = 2DE20B7F26409EB7004C597D /* BackendIntegrationTestsHostApp.app */; + productType = "com.apple.product-type.application"; + }; + 2DEAC2D926EFE46E006914ED /* UnitTestsHostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2DEAC2ED26EFE470006914ED /* Build configuration list for PBXNativeTarget "UnitTestsHostApp" */; + buildPhases = ( + 2DEAC2D626EFE46E006914ED /* Sources */, + 2DEAC2D826EFE46E006914ED /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UnitTestsHostApp; + productName = UnitTestsHostApp; + productReference = 2DEAC2DA26EFE46E006914ED /* UnitTestsHostApp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -1203,6 +1477,10 @@ LastUpgradeCheck = 1310; ORGANIZATIONNAME = RevenueCat; TargetAttributes = { + 2DAC5F7126F13C9800C5258F = { + CreatedOnToolsVersion = 13.0; + TestTargetID = 2DEAC2D926EFE46E006914ED; + }; 2DC5621524EC63420031F69B = { CreatedOnToolsVersion = 12.0; LastSwiftMigration = 1250; @@ -1222,6 +1500,9 @@ CreatedOnToolsVersion = 12.5; ProvisioningStyle = Automatic; }; + 2DEAC2D926EFE46E006914ED = { + CreatedOnToolsVersion = 12.5.1; + }; }; }; buildConfigurationList = 352629F81F7C4B9100C04F2C /* Build configuration list for PBXProject "RevenueCat" */; @@ -1234,8 +1515,8 @@ ); mainGroup = 352629F41F7C4B9100C04F2C; packageReferences = ( - B3D5CFBB267282630056FA67 /* XCRemoteSwiftPackageReference "Nimble" */, - B3D5CFBE267282760056FA67 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, + 2D803F6126F144830069D717 /* XCRemoteSwiftPackageReference "nimble" */, + 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, ); productRefGroup = 352629FF1F7C4B9100C04F2C /* Products */; projectDirPath = ""; @@ -1243,13 +1524,23 @@ targets = ( 2DC5621524EC63420031F69B /* RevenueCat */, 2DC5621D24EC63430031F69B /* RevenueCatTests */, - 2DE20B7E26409EB7004C597D /* StoreKitTestApp */, - 2DE20B6B264087FB004C597D /* StoreKitTests */, + 2DE20B7E26409EB7004C597D /* BackendIntegrationTestsHostApp */, + 2DE20B6B264087FB004C597D /* BackendIntegrationTests */, + 2DEAC2D926EFE46E006914ED /* UnitTestsHostApp */, + 2DAC5F7126F13C9800C5258F /* StoreKitUnitTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 2DAC5F7026F13C9800C5258F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DAC5F7B26F13D1400C5258F /* UnitTestsConfiguration.storekit in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2DC5621424EC63420031F69B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1272,7 +1563,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2DE20B7A26408921004C597D /* Configuration.storekit in Resources */, + 2D735F7D26EFF189004E82A7 /* Configuration.storekit in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1285,6 +1576,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2DEAC2D826EFE46E006914ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DEAC2E926EFE470006914ED /* LaunchScreen.storyboard in Resources */, + 2D735F7E26EFF198004E82A7 /* UnitTestsConfiguration.storekit in Resources */, + 2DEAC2E626EFE470006914ED /* Assets.xcassets in Resources */, + 2DEAC2E426EFE46E006914ED /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1327,6 +1629,49 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 2DAC5F6E26F13C9800C5258F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D9C5ECA26F2805C0057FC45 /* ProductsManagerTests.swift in Sources */, + 3543914526F926D900E669DF /* SKProductSubscriptionDurationExtensions.swift in Sources */, + 3543913926F90FFB00E669DF /* MockBackend.swift in Sources */, + 2D9C5EC826F280510057FC45 /* ProductDetailsTests.swift in Sources */, + 3543914426F911F300E669DF /* MockCustomerInfoManager.swift in Sources */, + 3543914226F911F300E669DF /* MockSK1Product.swift in Sources */, + 3543913826F90FE100E669DF /* MockIntroEligibilityCalculator.swift in Sources */, + 359E3ABF26FBE8CD0094617C /* PartialMockProductsManager.swift in Sources */, + 3543914126F911CC00E669DF /* MockDeviceCache.swift in Sources */, + 3543913C26F9101600E669DF /* MockOperationDispatcher.swift in Sources */, + 3543913626F90D6A00E669DF /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift in Sources */, + 2D34D9D227481D9B00C05DB6 /* TrialOrIntroPriceEligibilityCheckerSK2Tests.swift in Sources */, + 2D90F8C726FD221D009B9142 /* MockASN1ContainerBuilder.swift in Sources */, + 2D90F8C326FD214F009B9142 /* MockAttributionTypeFactory.swift in Sources */, + A55D083427235DED00D919E0 /* BeginRefundRequestHelperTests.swift in Sources */, + 2D90F8CC26FD2BA1009B9142 /* StoreKitConfigTestCase.swift in Sources */, + 2D90F8BC26FD20C2009B9142 /* MockReceiptParser.swift in Sources */, + 2D90F8C226FD20F7009B9142 /* MockETagManager.swift in Sources */, + 2D90F8B526FD2093009B9142 /* MockSystemInfo.swift in Sources */, + 2D90F8C126FD20F2009B9142 /* MockHTTPClient.swift in Sources */, + A55D083527235E2800D919E0 /* ManageSubscriptionsModalHelperTests.swift in Sources */, + 2DFF6C56270CA28800ECAFAB /* MockRequestFetcher.swift in Sources */, + 2D90F8C826FD2225009B9142 /* MockAppleReceiptBuilder.swift in Sources */, + 2D90F8C026FD20DF009B9142 /* MockAttributionDataMigrator.swift in Sources */, + 2D90F8B826FD20AA009B9142 /* MockReceiptFetcher.swift in Sources */, + 2D90F8B626FD2099009B9142 /* MockSubscriberAttributesManager.swift in Sources */, + 2D90F8BF26FD20D6009B9142 /* MockAttributionFetcher.swift in Sources */, + 2D90F8C526FD216A009B9142 /* MockSKDiscount.swift in Sources */, + 2D90F8B326FD2082009B9142 /* MockProductsManager.swift in Sources */, + 2D90F8CA26FD257A009B9142 /* MockStoreKit2TransactionListener.swift in Sources */, + 35B745A82711001A00458D46 /* MockManageSubscriptionsModalHelper.swift in Sources */, + 2D90F8B126FD1E52009B9142 /* PurchasesOrchestratorTests.swift in Sources */, + A563F589271E1DAD00246E0C /* MockBeginRefundRequestHelper.swift in Sources */, + 2D90F8B426FD208B009B9142 /* MockStoreKitWrapper.swift in Sources */, + 2D90F8BB26FD20BD009B9142 /* MockIdentityManager.swift in Sources */, + A55D083627236F0200D919E0 /* MockSK2BeginRefundRequestHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2DC5621224EC63420031F69B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1361,10 +1706,12 @@ 2D11F5E1250FF886005A70E8 /* AttributionStrings.swift in Sources */, 2CD72944268A826F00BFC976 /* Date+Extensions.swift in Sources */, B3AA6238268B926F00894871 /* SystemInfo.swift in Sources */, + 2D294E5C26DECFD500B8FE4F /* StoreKit2TransactionListener.swift in Sources */, 2DDF41B524F6F387005BC22D /* AppleReceiptBuilder.swift in Sources */, B3C4AAD526B8911300E1B3C8 /* Backend.swift in Sources */, B34D2AA626976FC700D88C3A /* ErrorCode.swift in Sources */, B39E811D268E887500D31189 /* SubscriberAttribute.swift in Sources */, + A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */, F5BE4479269E4A4C00254A30 /* AfficheClientProxy.swift in Sources */, B33CEAA0268CDCC9008A3144 /* ISOPeriodFormatter.swift in Sources */, 2DDF41A324F6F331005BC22D /* ReceiptParser.swift in Sources */, @@ -1409,6 +1756,7 @@ 2DC5622E24EC636C0031F69B /* Transaction.swift in Sources */, 9A65E0AA2591A23800DE00B0 /* RestoreStrings.swift in Sources */, 35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */, + A55D08302722368600D919E0 /* SK2BeginRefundRequestHelper.swift in Sources */, 35D832CD262A5B7500E60AC5 /* ETagManager.swift in Sources */, 2DDF41BC24F6F392005BC22D /* ArraySlice_UInt8+Extensions.swift in Sources */, F575858D26C088FE00C12B97 /* OfferingsManager.swift in Sources */, @@ -1419,7 +1767,9 @@ 2DDF41A224F6F331005BC22D /* ProductsManager.swift in Sources */, B3AA6236268A81C700894871 /* EntitlementInfos.swift in Sources */, B372EC56268FEF020099171E /* ProductInfo.swift in Sources */, + 359E8E3F26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift in Sources */, F5BE44432698581100254A30 /* AttributionTypeFactory.swift in Sources */, + 2D971CC12744364C0093F35F /* SKError+Extensions.swift in Sources */, 57EAE527274324C60060EB74 /* Lock.swift in Sources */, B32B74FF26868AEB005647BF /* Package.swift in Sources */, 2DDF41B324F6F387005BC22D /* InAppPurchaseBuilder.swift in Sources */, @@ -1440,6 +1790,10 @@ F5714EA526D6C24D00635477 /* JSONDecoder+Extensions.swift in Sources */, B302206A27271BCB008F1A0D /* Decoder+Extensions.swift in Sources */, B3766F1E26BDA95100141450 /* IntroEligibilityResponse.swift in Sources */, + 6E38843A0CAFD551013D0A3F /* ProductDetails.swift in Sources */, + 35E840CC270FB70D00899AE2 /* ManageSubscriptionsModalHelper.swift in Sources */, + 42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */, + 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1473,10 +1827,11 @@ 351B51B926D450E800BD2BD7 /* TransactionsFactoryTests.swift in Sources */, 351B51BB26D450E800BD2BD7 /* ProductInfoExtractorTests.swift in Sources */, 351B515426D44B0A00BD2BD7 /* MockStoreKitWrapper.swift in Sources */, + 35F8B8FA26E02AE1003C3363 /* MockTrialOrIntroPriceEligibilityChecker.swift in Sources */, 35D83300262FAD8000E60AC5 /* ETagManagerTests.swift in Sources */, 351B514126D4498F00BD2BD7 /* MockBackend.swift in Sources */, 351B513B26D448F400BD2BD7 /* AttributionPosterTests.swift in Sources */, - 351B51B526D450E800BD2BD7 /* ProductsManagerTests.swift in Sources */, + 351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */, 2DDF41CC24F6F4C3005BC22D /* AppleReceiptBuilderTests.swift in Sources */, 351B51C026D450E800BD2BD7 /* PurchasesTests.swift in Sources */, 2DDF41CD24F6F4C3005BC22D /* ASN1ContainerBuilderTests.swift in Sources */, @@ -1487,8 +1842,9 @@ 351B517226D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift in Sources */, 351B51A426D450BC00BD2BD7 /* NSError+RCExtensionsTests.swift in Sources */, 351B51B826D450E800BD2BD7 /* StoreKitWrapperTests.swift in Sources */, - 2DDF41E624F6F5DC005BC22D /* MockSKProduct.swift in Sources */, + 2DDF41E624F6F5DC005BC22D /* MockSK1Product.swift in Sources */, 351B51BA26D450E800BD2BD7 /* ProductInfoExtensions.swift in Sources */, + 35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsModalHelper.swift in Sources */, F591492826B9956C00D32E58 /* MockTransaction.swift in Sources */, 351B517626D44F7200BD2BD7 /* MockProductDiscount.swift in Sources */, 351B516A26D44CB300BD2BD7 /* ISOPeriodFormatterTests.swift in Sources */, @@ -1530,11 +1886,11 @@ 351B51BD26D450E800BD2BD7 /* EntitlementInfosTests.swift in Sources */, 351B514526D449E600BD2BD7 /* MockAttributionTypeFactory.swift in Sources */, F575858F26C0893600C12B97 /* MockOfferingsManager.swift in Sources */, + A563F586271E072B00246E0C /* MockBeginRefundRequestHelper.swift in Sources */, 2DA85A8A26DEA7DC00F1136D /* MockProductsRequestFactory.swift in Sources */, 351B516026D44BB600BD2BD7 /* MockAttributionDataMigrator.swift in Sources */, 2DDF41E024F6F527005BC22D /* MockInAppPurchaseBuilder.swift in Sources */, 37E352973B0901E3CAA717E1 /* DateFormatter+ExtensionsTests.swift in Sources */, - 351B51B726D450E800BD2BD7 /* RCIntroEligibilityTests.swift in Sources */, B3F8418F26F3A93400E560FB /* ErrorCodeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1546,8 +1902,8 @@ 2DA85A8B26DEA7DD00F1136D /* MockProductsRequestFactory.swift in Sources */, 2D3BFAD326DEA47100370B11 /* MockSKDiscount.swift in Sources */, 2D3BFAD426DEA49200370B11 /* SKProductSubscriptionDurationExtensions.swift in Sources */, - 2D3BFAD126DEA45C00370B11 /* MockSKProduct.swift in Sources */, - 2DE20B6F264087FB004C597D /* StoreKitTests.swift in Sources */, + 2DE20B6F264087FB004C597D /* BackendIntegrationTests.swift in Sources */, + 2D3BFAD126DEA45C00370B11 /* MockSK1Product.swift in Sources */, 2DE61A84264190830021CEA0 /* Constants.swift in Sources */, 2D3BFAD226DEA46600370B11 /* MockProductsRequest.swift in Sources */, ); @@ -1558,13 +1914,38 @@ buildActionMask = 2147483647; files = ( 2DE20B8426409EB7004C597D /* ContentView.swift in Sources */, - 2DE20B8226409EB7004C597D /* StoreKitTestAppApp.swift in Sources */, + 2DE20B8226409EB7004C597D /* BackendIntegrationTestApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2DEAC2D626EFE46E006914ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DEAC2E126EFE46E006914ED /* ViewController.swift in Sources */, + 2DEAC2DD26EFE46E006914ED /* AppDelegate.swift in Sources */, + 2D1E789926FE216800760949 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 2D803F6B26F150190069D717 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2DC5621524EC63420031F69B /* RevenueCat */; + targetProxy = 2D803F6A26F150190069D717 /* PBXContainerItemProxy */; + }; + 2D803F6D26F150200069D717 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2DC5621524EC63420031F69B /* RevenueCat */; + targetProxy = 2D803F6C26F150200069D717 /* PBXContainerItemProxy */; + }; + 2DAC5F7726F13C9800C5258F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2DEAC2D926EFE46E006914ED /* UnitTestsHostApp */; + targetProxy = 2DAC5F7626F13C9800C5258F /* PBXContainerItemProxy */; + }; 2DC5622124EC63430031F69B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2DC5621524EC63420031F69B /* RevenueCat */; @@ -1572,12 +1953,97 @@ }; 2DE20B8F26409EC0004C597D /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2DE20B7E26409EB7004C597D /* StoreKitTestApp */; + target = 2DE20B7E26409EB7004C597D /* BackendIntegrationTestsHostApp */; targetProxy = 2DE20B8E26409EC0004C597D /* PBXContainerItemProxy */; }; + 2DFF6C55270CA11400ECAFAB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2DEAC2D926EFE46E006914ED /* UnitTestsHostApp */; + targetProxy = 2DFF6C54270CA11400ECAFAB /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 2DEAC2E226EFE46E006914ED /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DEAC2E326EFE46E006914ED /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 2DEAC2E726EFE470006914ED /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2DEAC2E826EFE470006914ED /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + 2DAC5F7926F13C9800C5258F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestsHostApp.app/UnitTestsHostApp"; + TVOS_DEPLOYMENT_TARGET = 14.1; + WATCHOS_DEPLOYMENT_TARGET = 6.2; + }; + name = Debug; + }; + 2DAC5F7A26F13C9800C5258F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestsHostApp.app/UnitTestsHostApp"; + TVOS_DEPLOYMENT_TARGET = 14.1; + WATCHOS_DEPLOYMENT_TARGET = 6.2; + }; + name = Release; + }; 2DC5622824EC63430031F69B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1593,7 +2059,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Purchases/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1632,7 +2098,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Purchases/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1657,6 +2123,7 @@ 2DC5622B24EC63430031F69B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1664,7 +2131,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8SXR2327BM; INFOPLIST_FILE = PurchasesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1675,8 +2142,9 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PurchasesTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx appletvsimulator appletvos"; + SDKROOT = ""; + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos"; + SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1690,6 +2158,7 @@ 2DC5622C24EC63430031F69B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1697,7 +2166,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8SXR2327BM; INFOPLIST_FILE = PurchasesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1707,8 +2176,9 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PurchasesTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx appletvsimulator appletvos"; + SDKROOT = ""; + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos"; + SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,6"; @@ -1723,23 +2193,23 @@ CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = StoreKitTests/Info.plist; + INFOPLIST_FILE = BackendIntegrationTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 11.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitTests; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.BackendIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StoreKitTestApp.app/StoreKitTestApp"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BackendIntegrationTestsHostApp.app/BackendIntegrationTestsHostApp"; TVOS_DEPLOYMENT_TARGET = 14.1; WATCHOS_DEPLOYMENT_TARGET = 7.1; }; @@ -1751,20 +2221,20 @@ CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = StoreKitTests/Info.plist; + INFOPLIST_FILE = BackendIntegrationTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 11.3; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitTests; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.BackendIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StoreKitTestApp.app/StoreKitTestApp"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BackendIntegrationTestsHostApp.app/BackendIntegrationTestsHostApp"; TVOS_DEPLOYMENT_TARGET = 14.1; WATCHOS_DEPLOYMENT_TARGET = 7.1; }; @@ -1778,10 +2248,10 @@ CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"StoreKitTestApp/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"BackendIntegrationTestApp/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = StoreKitTestApp/Info.plist; + INFOPLIST_FILE = BackendIntegrationTestApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1795,6 +2265,8 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 14.1; + WATCHOS_DEPLOYMENT_TARGET = 6.2; }; name = Debug; }; @@ -1806,10 +2278,10 @@ CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"StoreKitTestApp/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"BackendIntegrationTestApp/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = StoreKitTestApp/Info.plist; + INFOPLIST_FILE = BackendIntegrationTestApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1820,6 +2292,61 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 14.1; + WATCHOS_DEPLOYMENT_TARGET = 6.2; + }; + name = Release; + }; + 2DEAC2EB26EFE470006914ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = UnitTestsHostApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitUnitTestsHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 6.2; + }; + name = Debug; + }; + 2DEAC2EC26EFE470006914ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = UnitTestsHostApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.2; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.StoreKitUnitTestsHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 6.2; }; name = Release; }; @@ -1943,6 +2470,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2DAC5F7826F13C9800C5258F /* Build configuration list for PBXNativeTarget "StoreKitUnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2DAC5F7926F13C9800C5258F /* Debug */, + 2DAC5F7A26F13C9800C5258F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 2DC5622724EC63430031F69B /* Build configuration list for PBXNativeTarget "RevenueCat" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1961,7 +2497,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2DE20B73264087FB004C597D /* Build configuration list for PBXNativeTarget "StoreKitTests" */ = { + 2DE20B73264087FB004C597D /* Build configuration list for PBXNativeTarget "BackendIntegrationTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 2DE20B71264087FB004C597D /* Debug */, @@ -1970,7 +2506,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2DE20B8B26409EB8004C597D /* Build configuration list for PBXNativeTarget "StoreKitTestApp" */ = { + 2DE20B8B26409EB8004C597D /* Build configuration list for PBXNativeTarget "BackendIntegrationTestsHostApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 2DE20B8C26409EB8004C597D /* Debug */, @@ -1979,6 +2515,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2DEAC2ED26EFE470006914ED /* Build configuration list for PBXNativeTarget "UnitTestsHostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2DEAC2EB26EFE470006914ED /* Debug */, + 2DEAC2EC26EFE470006914ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 352629F81F7C4B9100C04F2C /* Build configuration list for PBXProject "RevenueCat" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1991,44 +2536,59 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - B3D5CFBB267282630056FA67 /* XCRemoteSwiftPackageReference "Nimble" */ = { + 2D803F6126F144830069D717 /* XCRemoteSwiftPackageReference "nimble" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Quick/Nimble.git"; + repositoryURL = "https://github.com/quick/nimble"; requirement = { - kind = revision; - revision = 0fd4ba405699eb8ca939aefa34babf988f4ff9ff; + branch = main; + kind = branch; }; }; - B3D5CFBE267282760056FA67 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { + 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/AliSoftware/OHHTTPStubs.git"; requirement = { - kind = exactVersion; - version = 9.1.0; + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B3D5CFC32672827D0056FA67 /* Nimble */ = { + 2D803F6226F144830069D717 /* Nimble */ = { isa = XCSwiftPackageProductDependency; - package = B3D5CFBB267282630056FA67 /* XCRemoteSwiftPackageReference "Nimble" */; + package = 2D803F6126F144830069D717 /* XCRemoteSwiftPackageReference "nimble" */; productName = Nimble; }; - B3D5CFC52672827D0056FA67 /* OHHTTPStubs */ = { + 2D803F6526F144BF0069D717 /* Nimble */ = { isa = XCSwiftPackageProductDependency; - package = B3D5CFBE267282760056FA67 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + package = 2D803F6126F144830069D717 /* XCRemoteSwiftPackageReference "nimble" */; + productName = Nimble; + }; + 2D803F6726F144C40069D717 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = 2D803F6126F144830069D717 /* XCRemoteSwiftPackageReference "nimble" */; + productName = Nimble; + }; + 2D9C5ED026F2816F0057FC45 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + package = 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; productName = OHHTTPStubs; }; - B3D5CFC72672827D0056FA67 /* OHHTTPStubsSwift */ = { + 2D9C5ED226F2816F0057FC45 /* OHHTTPStubsSwift */ = { isa = XCSwiftPackageProductDependency; - package = B3D5CFBE267282760056FA67 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + package = 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; productName = OHHTTPStubsSwift; }; - B3D5CFC9267282860056FA67 /* Nimble */ = { + 2D9C5ED426F281750057FC45 /* OHHTTPStubs */ = { isa = XCSwiftPackageProductDependency; - package = B3D5CFBB267282630056FA67 /* XCRemoteSwiftPackageReference "Nimble" */; - productName = Nimble; + package = 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubs; + }; + 2D9C5ED626F281750057FC45 /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 2D9C5ECB26F2815E0057FC45 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubsSwift; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/RevenueCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RevenueCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 8bb96b9945..0000000000 --- a/RevenueCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,43 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", - "version": "2.1.1" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "fb7a26374e8570ff5c68142e5c83406d6abae0d8", - "version": "2.0.2" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "0fd4ba405699eb8ca939aefa34babf988f4ff9ff", - "version": null - } - }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", - "state": { - "branch": null, - "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version": "9.1.0" - } - } - ] - }, - "version": 1 -} diff --git a/RevenueCat.xcodeproj/xcshareddata/xcschemes/StoreKitTests.xcscheme b/RevenueCat.xcodeproj/xcshareddata/xcschemes/BackendIntegrationTests.xcscheme similarity index 88% rename from RevenueCat.xcodeproj/xcshareddata/xcschemes/StoreKitTests.xcscheme rename to RevenueCat.xcodeproj/xcshareddata/xcschemes/BackendIntegrationTests.xcscheme index 4e41024aa1..c7b28d467c 100644 --- a/RevenueCat.xcodeproj/xcshareddata/xcschemes/StoreKitTests.xcscheme +++ b/RevenueCat.xcodeproj/xcshareddata/xcschemes/BackendIntegrationTests.xcscheme @@ -15,8 +15,8 @@ @@ -34,8 +34,8 @@ @@ -56,8 +56,8 @@ diff --git a/RevenueCat.xcodeproj/xcshareddata/xcschemes/RevenueCat.xcscheme b/RevenueCat.xcodeproj/xcshareddata/xcschemes/RevenueCat.xcscheme index 5c23a05e25..8de02c4f75 100644 --- a/RevenueCat.xcodeproj/xcshareddata/xcschemes/RevenueCat.xcscheme +++ b/RevenueCat.xcodeproj/xcshareddata/xcschemes/RevenueCat.xcscheme @@ -34,6 +34,20 @@ ReferencedContainer = "container:RevenueCat.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StoreKitUnitTests/Mocks/PartialMockProductsManager.swift b/StoreKitUnitTests/Mocks/PartialMockProductsManager.swift new file mode 100644 index 0000000000..14c90ac6fa --- /dev/null +++ b/StoreKitUnitTests/Mocks/PartialMockProductsManager.swift @@ -0,0 +1,99 @@ +// +// 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 +// +// PartialMockProductsManager.swift +// +// Created by César de la Vega on 9/22/21. + +import Foundation +@testable import RevenueCat +import StoreKit + +class PartialMockProductsManager: ProductsManager { + + var invokedProductsFromOptimalStoreKitVersionWithIdentifiers = false + var invokedProductsFromOptimalStoreKitVersionCount = 0 + var invokedProductsFromOptimalStoreKitVersionParameters: (identifiers: Set, Void)? + var invokedProductsFromOptimalStoreKitVersionParametersList = [(identifiers: Set, Void)]() + var stubbedProductsFromOptimalStoreKitVersionCompletionResult: (Set, Void)? + + override func productsFromOptimalStoreKitVersion(withIdentifiers identifiers: Set, + completion: @escaping (Set) -> Void) { + invokedProductsFromOptimalStoreKitVersionWithIdentifiers = true + invokedProductsFromOptimalStoreKitVersionCount += 1 + invokedProductsFromOptimalStoreKitVersionParameters = (identifiers, ()) + invokedProductsFromOptimalStoreKitVersionParametersList.append((identifiers, ())) + if let result = stubbedProductsFromOptimalStoreKitVersionCompletionResult { + completion(result.0) + } else { + let products: [SK1Product] = identifiers.map { (identifier) -> MockSK1Product in + let sk1Product = MockSK1Product(mockProductIdentifier: identifier) + sk1Product.mockSubscriptionGroupIdentifier = "1234567" + if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { + let mockDiscount = MockDiscount() + mockDiscount.mockIdentifier = "discount_id" + sk1Product.mockDiscount = mockDiscount + } + return sk1Product + } + let result = Set(products).map { SK1ProductDetails(sk1Product: $0) } + + completion(Set(result)) + } + } + + var invokedProducts = false + var invokedProductsCount = 0 + var invokedProductsParameters: Set? + var invokedProductsParametersList = [Set]() + var stubbedProductsCompletionResult: Set? + + override func products(withIdentifiers identifiers: Set, completion: @escaping (Set) -> Void) { + invokedProducts = true + invokedProductsCount += 1 + invokedProductsParameters = identifiers + invokedProductsParametersList.append(identifiers) + if let result = stubbedProductsCompletionResult { + completion(result) + } else { + let products: [SK1Product] = identifiers.map { (identifier) -> MockSK1Product in + let sk1Product = MockSK1Product(mockProductIdentifier: identifier) + sk1Product.mockSubscriptionGroupIdentifier = "1234567" + if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { + let mockDiscount = MockDiscount() + mockDiscount.mockIdentifier = "discount_id" + sk1Product.mockDiscount = mockDiscount + } + return sk1Product + } + completion(Set(products)) + } + } + + var invokedCacheProduct = false + var invokedCacheProductCount = 0 + var invokedCacheProductParameter: SK1Product? + + override func cacheProduct(_ product: SK1Product) { + invokedCacheProduct = true + invokedCacheProductCount += 1 + invokedCacheProductParameter = product + } + + func resetMock() { + invokedProducts = false + invokedProductsCount = 0 + invokedProductsParameters = nil + invokedProductsParametersList = [] + stubbedProductsCompletionResult = nil + invokedCacheProduct = false + invokedCacheProductCount = 0 + invokedCacheProductParameter = nil + } +} diff --git a/StoreKitUnitTests/ProductDetailsTests.swift b/StoreKitUnitTests/ProductDetailsTests.swift new file mode 100644 index 0000000000..66ef4fbd67 --- /dev/null +++ b/StoreKitUnitTests/ProductDetailsTests.swift @@ -0,0 +1,119 @@ +// +// 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 +// +// ProductDetailsTests.swift +// +// Created by Andrés Boedo on 1/9/21. + +import Nimble +@testable import RevenueCat +import StoreKitTest +import XCTest + +class ProductDetailsTests: StoreKitConfigTestCase { + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func testSK1AndSK2DetailsAreEquivalent() async throws { + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let productIdentifiers = Set([ + "com.revenuecat.monthly_4.99.1_week_intro", + "com.revenuecat.annual_39.99.2_week_intro", + "lifetime" + ]) + let sk1Fetcher = ProductsFetcherSK1(productsRequestFactory: ProductsRequestFactory()) + let sk1ProductDetails = await sk1Fetcher.products(withIdentifiers: productIdentifiers) + let sk1ProductDetailsByID = sk1ProductDetails.reduce(into: [:]) { partialResult, wrapper in + partialResult[wrapper.productIdentifier] = wrapper + } + + let sk2Fetcher = ProductsFetcherSK2() + let sk2ProductDetails = try await sk2Fetcher.products(identifiers: productIdentifiers) + let sk2ProductDetailsByID = sk2ProductDetails.reduce(into: [:]) { partialResult, wrapper in + partialResult[wrapper.productIdentifier] = wrapper + } + + expect(sk1ProductDetails.count) == productIdentifiers.count + expect(sk1ProductDetails.count) == sk2ProductDetails.count + + for sk1ProductID in sk1ProductDetailsByID.keys { + let sk1Product = try XCTUnwrap(sk1ProductDetailsByID[sk1ProductID]) + let equivalentSK2Product = try XCTUnwrap(sk2ProductDetailsByID[sk1ProductID]) + + expect(sk1Product.productIdentifier) == equivalentSK2Product.productIdentifier + expect(sk1Product.localizedDescription) == equivalentSK2Product.localizedDescription + expect(sk1Product.price) == equivalentSK2Product.price + expect(sk1Product.localizedPriceString) == equivalentSK2Product.localizedPriceString + expect(sk1Product.productIdentifier) == equivalentSK2Product.productIdentifier + expect(sk1Product.isFamilyShareable) == equivalentSK2Product.isFamilyShareable + expect(sk1Product.localizedTitle) == equivalentSK2Product.localizedTitle + if sk1Product.subscriptionGroupIdentifier != nil { + expect(sk1Product.subscriptionGroupIdentifier) == equivalentSK2Product.subscriptionGroupIdentifier + } else { + expect(equivalentSK2Product.subscriptionGroupIdentifier).to(beNil()) + } + } + } + + func testSk1DetailsWrapsCorrectly() throws { + let productIdentifier = "com.revenuecat.monthly_4.99.1_week_intro" + let sk1Fetcher = ProductsFetcherSK1(productsRequestFactory: ProductsRequestFactory()) + var callbackCalled = false + + sk1Fetcher.products(withIdentifiers: Set([productIdentifier])) { productDetailsSet in + callbackCalled = true + guard let productDetails = productDetailsSet.first else { fatalError("couldn't get product!") } + + expect(productDetails.productIdentifier) == "com.revenuecat.monthly_4.99.1_week_intro" + expect(productDetails.localizedDescription) == "Monthly subscription with a 1-week free trial" + expect(productDetails.price.description) == "4.99" + expect(productDetails.localizedPriceString) == "$4.99" + expect(productDetails.productIdentifier) == productIdentifier + expect(productDetails.isFamilyShareable) == true + expect(productDetails.localizedTitle) == "Monthly Free Trial" + // open the StoreKit Config file as source code to see the expected value + expect(productDetails.subscriptionGroupIdentifier) == "7096FF06" + } + + expect(callbackCalled).toEventually(beTrue(), timeout: .seconds(5)) + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func testSk2DetailsWrapsCorrectly() async throws { + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let productIdentifier = "com.revenuecat.monthly_4.99.1_week_intro" + let sk2Fetcher = ProductsFetcherSK2() + + let productDetailsSet = try await sk2Fetcher.products(identifiers: Set([productIdentifier])) + + let productDetails = try XCTUnwrap(productDetailsSet.first) + + expect(productDetails.productIdentifier) == "com.revenuecat.monthly_4.99.1_week_intro" + expect(productDetails.localizedDescription) == "Monthly subscription with a 1-week free trial" + expect(productDetails.price.description) == "4.99" + expect(productDetails.localizedPriceString) == "$4.99" + expect(productDetails.productIdentifier) == productIdentifier + expect(productDetails.isFamilyShareable) == true + expect(productDetails.localizedTitle) == "Monthly Free Trial" + // open the StoreKit Config file as source code to see the expected value + expect(productDetails.subscriptionGroupIdentifier) == "7096FF06" + } + +} diff --git a/StoreKitUnitTests/ProductsManagerTests.swift b/StoreKitUnitTests/ProductsManagerTests.swift new file mode 100644 index 0000000000..eaa527960d --- /dev/null +++ b/StoreKitUnitTests/ProductsManagerTests.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 +// +// ProductsManagerTests.swift +// +// Created by Andrés Boedo on 7/23/21. + +import Nimble +@testable import RevenueCat +import StoreKitTest +import XCTest + +// - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: +// Although the method isn't supposed to be called because of our @available marks, +// everything in this class will still be called by XCTest, and it will cause errors. +@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 6.2, *) +class ProductsManagerTests: StoreKitConfigTestCase { + + var productsManager: ProductsManager! + var systemInfo: MockSystemInfo! + + override func setUpWithError() throws { + try super.setUpWithError() + systemInfo = try MockSystemInfo(platformFlavor: "xyz", platformFlavorVersion: "123", finishTransactions: true) + productsManager = ProductsManager(systemInfo: systemInfo) + } + + func testFetchProductsFromOptimalStoreKitVersion() throws { + guard #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 6.2, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let identifier = "com.revenuecat.monthly_4.99.1_week_intro" + var completionCalled = false + var maybeReceivedProducts: Set? + + productsManager.productsFromOptimalStoreKitVersion(withIdentifiers: Set([identifier]), completion: { products in + completionCalled = true + maybeReceivedProducts = products + }) + + expect(completionCalled).toEventually(beTrue()) + let receivedProducts = try XCTUnwrap(maybeReceivedProducts) + expect(receivedProducts.count) == 1 + + let firstProduct = try XCTUnwrap(receivedProducts.first) + + if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 7.0, *), + systemInfo.useStoreKit2IfAvailable { + expect(firstProduct as? SK2ProductDetails).toNot(beNil()) + } else { + expect(firstProduct as? SK1ProductDetails).toNot(beNil()) + } + expect(firstProduct.productIdentifier) == identifier + } + + func testFetchProductsFromOptimalStoreKitVersionPrefersSK2IfUseSK2WhenAvailableEnabled() throws { + + systemInfo = try MockSystemInfo(platformFlavor: "xyz", + platformFlavorVersion: "123", + finishTransactions: true, + useStoreKit2IfAvailable: true) + productsManager = ProductsManager(systemInfo: systemInfo) + + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 7.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let identifier = "com.revenuecat.monthly_4.99.1_week_intro" + var completionCalled = false + var maybeReceivedProducts: Set? + + productsManager.productsFromOptimalStoreKitVersion(withIdentifiers: Set([identifier]), completion: { products in + completionCalled = true + maybeReceivedProducts = products + }) + + expect(completionCalled).toEventually(beTrue()) + let receivedProducts = try XCTUnwrap(maybeReceivedProducts) + expect(receivedProducts.count) == 1 + + let firstProduct = try XCTUnwrap(receivedProducts.first) + + expect(firstProduct).to(beAnInstanceOf(SK2ProductDetails.self)) + } + + func testFetchProductsFromOptimalStoreKitVersionPrefersSK1ByDefault() throws { + + systemInfo = try MockSystemInfo(platformFlavor: "xyz", + platformFlavorVersion: "123", + finishTransactions: true) + productsManager = ProductsManager(systemInfo: systemInfo) + + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 7.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let identifier = "com.revenuecat.monthly_4.99.1_week_intro" + var completionCalled = false + var maybeReceivedProducts: Set? + + productsManager.productsFromOptimalStoreKitVersion(withIdentifiers: Set([identifier]), completion: { products in + completionCalled = true + maybeReceivedProducts = products + }) + + expect(completionCalled).toEventually(beTrue()) + let receivedProducts = try XCTUnwrap(maybeReceivedProducts) + expect(receivedProducts.count) == 1 + + let firstProduct = try XCTUnwrap(receivedProducts.first) + + expect(firstProduct as? SK1ProductDetails).toNot(beNil()) + expect(firstProduct).to(beAnInstanceOf(SK1ProductDetails.self)) + } + +} diff --git a/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/StoreKitUnitTests/PurchasesOrchestratorTests.swift new file mode 100644 index 0000000000..c262392e8f --- /dev/null +++ b/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -0,0 +1,319 @@ +// +// 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 +// +// PurchasesOrchestratorTests.swift +// +// Created by Andrés Boedo on 1/9/21. + +import Foundation +import Nimble +@testable import RevenueCat +import StoreKit +import XCTest + +class PurchasesOrchestratorTests: StoreKitConfigTestCase { + + var productsManager: MockProductsManager! + var storeKitWrapper: MockStoreKitWrapper! + var systemInfo: MockSystemInfo! + var subscriberAttributesManager: MockSubscriberAttributesManager! + var operationDispatcher: MockOperationDispatcher! + var receiptFetcher: MockReceiptFetcher! + var customerInfoManager: MockCustomerInfoManager! + var backend: MockBackend! + var identityManager: MockIdentityManager! + var receiptParser: MockReceiptParser! + var deviceCache: MockDeviceCache! + var mockManageSubsModalHelper: MockManageSubscriptionsModalHelper! + var mockBeginRefundRequestHelper: MockBeginRefundRequestHelper! + + var orchestrator: PurchasesOrchestrator! + + override func setUpWithError() throws { + try super.setUpWithError() + try setUpSystemInfo() + productsManager = MockProductsManager(systemInfo: systemInfo) + storeKitWrapper = MockStoreKitWrapper() + operationDispatcher = MockOperationDispatcher() + receiptFetcher = MockReceiptFetcher(requestFetcher: MockRequestFetcher(), systemInfo: systemInfo) + deviceCache = MockDeviceCache() + backend = MockBackend() + customerInfoManager = MockCustomerInfoManager(operationDispatcher: OperationDispatcher(), + deviceCache: deviceCache, + backend: backend, + systemInfo: systemInfo) + identityManager = MockIdentityManager(mockAppUserID: "appUserID") + receiptParser = MockReceiptParser() + let attributionFetcher = MockAttributionFetcher(attributionFactory: MockAttributionTypeFactory(), + systemInfo: systemInfo) + subscriberAttributesManager = MockSubscriberAttributesManager( + backend: backend, + deviceCache: deviceCache, + attributionFetcher: attributionFetcher, + attributionDataMigrator: MockAttributionDataMigrator()) + mockManageSubsModalHelper = MockManageSubscriptionsModalHelper(systemInfo: systemInfo, + customerInfoManager: customerInfoManager, + identityManager: identityManager) + mockBeginRefundRequestHelper = MockBeginRefundRequestHelper(systemInfo: systemInfo) + orchestrator = PurchasesOrchestrator(productsManager: productsManager, + storeKitWrapper: storeKitWrapper, + systemInfo: systemInfo, + subscriberAttributesManager: subscriberAttributesManager, + operationDispatcher: operationDispatcher, + receiptFetcher: receiptFetcher, + customerInfoManager: customerInfoManager, + backend: backend, + identityManager: identityManager, + receiptParser: receiptParser, + deviceCache: deviceCache, + manageSubscriptionsModalHelper: mockManageSubsModalHelper, + beginRefundRequestHelper: mockBeginRefundRequestHelper) + setUpStoreKit2Listener() + } + + fileprivate func setUpStoreKit2Listener() { + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) { + orchestrator.storeKit2Listener = MockStoreKit2TransactionListener() + } + } + + fileprivate func setUpSystemInfo() throws { + systemInfo = try MockSystemInfo(platformFlavor: "xyz", + platformFlavorVersion: "1.2.3", + finishTransactions: true) + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testPurchaseSK2PackageReturnsCorrectValues() async throws { + + guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo + backend.stubbedPostReceiptCustomerInfo = mockCustomerInfo + + let productDetails = try await fetchSk2ProductDetails() + let package = Package(identifier: "package", + packageType: .monthly, + productDetails: productDetails, + offeringIdentifier: "offering") + + let (transaction, customerInfo, error, userCancelled) = await withCheckedContinuation { continuation in + orchestrator.purchase(package: package) { transaction, customerInfo, error, userCancelled in + continuation.resume(returning: (transaction, customerInfo, error, userCancelled)) + } + } + + expect(transaction).to(beNil()) + expect(userCancelled) == false + expect(error).to(beNil()) + + let expectedCustomerInfo = try CustomerInfo(data: [ + "request_date": "2019-08-16T10:30:42Z", + "subscriber": [ + "first_seen": "2019-07-17T00:05:54Z", + "original_app_user_id": "", + "subscriptions": [:], + "other_purchases": [:] + ]]) + expect(customerInfo) == expectedCustomerInfo + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testPurchaseSK2PackageHandlesPurchaseResult() async throws { + guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo + backend.stubbedPostReceiptCustomerInfo = mockCustomerInfo + + let productDetails = try await fetchSk2ProductDetails() + let package = Package(identifier: "package", + packageType: .monthly, + productDetails: productDetails, + offeringIdentifier: "offering") + + _ = await withCheckedContinuation { continuation in + orchestrator.purchase(package: package) { transaction, customerInfo, error, userCancelled in + continuation.resume(returning: (transaction, customerInfo, error, userCancelled)) + } + } + + let mockListener = try XCTUnwrap(orchestrator.storeKit2Listener as? MockStoreKit2TransactionListener) + expect(mockListener.invokedHandle) == true + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testPurchaseSK2PackageSendsReceiptToBackendIfSuccessful() async throws { + guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo + backend.stubbedPostReceiptCustomerInfo = mockCustomerInfo + + let productDetails = try await fetchSk2ProductDetails() + let package = Package(identifier: "package", + packageType: .monthly, + productDetails: productDetails, + offeringIdentifier: "offering") + + _ = await withCheckedContinuation { continuation in + orchestrator.purchase(package: package) { transaction, customerInfo, error, userCancelled in + continuation.resume(returning: (transaction, customerInfo, error, userCancelled)) + } + } + + expect(self.backend.invokedPostReceiptDataCount) == 1 + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testPurchaseSK2PackageSkipsIfPurchaseFailed() async throws { + guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + guard self.systemInfo.useStoreKit2IfAvailable else { + throw XCTSkip("StoreKit 2 tests are disabled.") + } + + testSession.failTransactionsEnabled = true + customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo + backend.stubbedPostReceiptCustomerInfo = mockCustomerInfo + + let productDetails = try await fetchSk2ProductDetails() + let package = Package(identifier: "package", + packageType: .monthly, + productDetails: productDetails, + offeringIdentifier: "offering") + + let (transaction, customerInfo, error, userCancelled) = await withCheckedContinuation { continuation in + orchestrator.purchase(package: package) { transaction, customerInfo, error, userCancelled in + continuation.resume(returning: (transaction, customerInfo, error, userCancelled)) + } + } + + expect(transaction).to(beNil()) + expect(userCancelled) == false + expect(customerInfo).to(beNil()) + expect(error).toNot(beNil()) + expect(self.backend.invokedPostReceiptData) == false + let mockListener = try XCTUnwrap(orchestrator.storeKit2Listener as? MockStoreKit2TransactionListener) + expect(mockListener.invokedHandle) == false + } + + func testShowManageSubscriptionModalCallsCompletionWithErrorIfThereIsAFailure() { + let message = "Failed to get managemementURL from PurchaserInfo. Details: customerInfo is nil." + mockManageSubsModalHelper.mockError = ErrorUtils.customerInfoError(withMessage: message) + var receivedError: Error? + var completionCalled = false + orchestrator.showManageSubscriptionModal { maybeError in + completionCalled = true + receivedError = maybeError + } + + expect(completionCalled).toEventually(beTrue()) + expect(receivedError).toNot(beNil()) + expect(receivedError).to(matchError(ErrorCode.customerInfoError)) + } + + @available(iOS 15.0, macCatalyst 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + func testBeginRefundRequestCallsCompletionWithoutErrorAndPassesThroughStatusIfSuccessful() { + var receivedError: Error? + var receivedStatus: RefundRequestStatus? + var completionCalled = false + let expectedStatus = RefundRequestStatus.userCancelled + mockBeginRefundRequestHelper.maybeMockRefundRequestStatus = expectedStatus + + orchestrator.beginRefundRequest(for: "1234") { status, maybeError in + completionCalled = true + receivedError = maybeError + receivedStatus = status + } + + expect(receivedStatus) == expectedStatus + expect(completionCalled).toEventually(beTrue()) + expect(receivedError).to(beNil()) + } + + @available(iOS 15.0, macCatalyst 15.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + func testBeginRefundRequestCallsCompletionWithErrorIfThereIsAFailure() { + let expectedError = ErrorUtils.beginRefundRequestError(withMessage: "test") + mockBeginRefundRequestHelper.maybeMockError = expectedError + + var receivedError: Error? + var completionCalled = false + var receivedStatus: RefundRequestStatus? + + orchestrator.beginRefundRequest(for: "1235") { status, maybeError in + completionCalled = true + receivedError = maybeError + receivedStatus = status + } + + expect(completionCalled).toEventually(beTrue()) + expect(receivedError).toNot(beNil()) + expect(receivedStatus) == RefundRequestStatus.error + expect(receivedError).to(matchError(expectedError)) + } + +} + +private extension PurchasesOrchestratorTests { + + @MainActor + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func fetchSk2Product() async throws -> SK2Product { + let products: [Any] = try await StoreKit.Product.products(for: ["com.revenuecat.monthly_4.99.1_week_intro"]) + let firstProduct = try XCTUnwrap(products.first) + return try XCTUnwrap(firstProduct as? SK2Product) + } + + @MainActor + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func fetchSk2ProductDetails() async throws -> SK2ProductDetails { + // can't store SK2ProductDetails directly because it causes linking issues on older OS versions + // https://openradar.appspot.com/radar?id=4970535809187840 + let sk2Product: Any = try await fetchSk2Product() + return SK2ProductDetails(sk2Product: try XCTUnwrap(sk2Product as? SK2Product)) + } + + var mockCustomerInfo: CustomerInfo { + // swiftlint:disable:next force_try + try! CustomerInfo(data: [ + "request_date": "2019-08-16T10:30:42Z", + "subscriber": [ + "first_seen": "2019-07-17T00:05:54Z", + "original_app_user_id": "", + "subscriptions": [:], + "other_purchases": [:] + ]]) + } + +} diff --git a/StoreKitUnitTests/StoreKitConfigTestCase.swift b/StoreKitUnitTests/StoreKitConfigTestCase.swift new file mode 100644 index 0000000000..3b9a1f1c58 --- /dev/null +++ b/StoreKitUnitTests/StoreKitConfigTestCase.swift @@ -0,0 +1,58 @@ +// +// 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 +// +// StoreKitConfigTestCase.swift +// +// Created by Andrés Boedo on 23/9/21. + +import Foundation +import Nimble +@testable import RevenueCat +import StoreKitTest +import XCTest + +@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 6.2, *) +class StoreKitConfigTestCase: XCTestCase { + + private static var hasWaited = false + private static let waitLock = Lock() + private static let waitTimeInSeconds: Double = 20 + + var testSession: SKTestSession! + var userDefaults: UserDefaults! + + override func setUpWithError() throws { + testSession = try SKTestSession(configurationFileNamed: "UnitTestsConfiguration") + testSession.resetToDefaultState() + testSession.disableDialogs = true + testSession.clearTransactions() + + self.waitForStoreKitTestIfNeeded() + + let suiteName = "StoreKitConfigTests" + userDefaults = UserDefaults(suiteName: suiteName) + userDefaults.removePersistentDomain(forName: suiteName) + } + +} + +private extension StoreKitConfigTestCase { + + func waitForStoreKitTestIfNeeded() { + // StoreKitTest seems to take a few seconds to initialize, and running tests before that + // might result in failure. So we give it a few seconds to load before testing. + Self.waitLock.perform { + if !Self.hasWaited { + Self.hasWaited = true + Thread.sleep(forTimeInterval: Self.waitTimeInSeconds) + } + } + } + +} diff --git a/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK1Tests.swift b/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK1Tests.swift new file mode 100644 index 0000000000..f16807f4f0 --- /dev/null +++ b/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK1Tests.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 +// +// TrialOrIntroPriceEligibilityCheckerSK1Tests.swift +// +// Created by César de la Vega on 9/1/21. + +import Nimble +@testable import RevenueCat +import StoreKitTest +import XCTest + +// swiftlint:disable:next type_name +class TrialOrIntroPriceEligibilityCheckerSK1Tests: StoreKitConfigTestCase { + + var receiptFetcher: MockReceiptFetcher! + var trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityChecker! + var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! + var mockBackend: MockBackend! + var mockProductsManager: MockProductsManager! + var mockSystemInfo: MockSystemInfo! + + override func setUpWithError() throws { + try super.setUpWithError() + mockSystemInfo = try MockSystemInfo(platformFlavor: "xyz", + platformFlavorVersion: "123", + finishTransactions: true) + receiptFetcher = MockReceiptFetcher(requestFetcher: MockRequestFetcher(), systemInfo: mockSystemInfo) + let mockProductsManager = MockProductsManager(systemInfo: mockSystemInfo) + mockIntroEligibilityCalculator = MockIntroEligibilityCalculator(productsManager: mockProductsManager, + receiptParser: MockReceiptParser()) + mockBackend = MockBackend() + let mockOperationDispatcher = MockOperationDispatcher() + let identityManager = MockIdentityManager(mockAppUserID: "app_user") + trialOrIntroPriceEligibilityChecker = + TrialOrIntroPriceEligibilityChecker(receiptFetcher: receiptFetcher, + introEligibilityCalculator: mockIntroEligibilityCalculator, + backend: mockBackend, + identityManager: identityManager, + operationDispatcher: mockOperationDispatcher, + productsManager: mockProductsManager) + } + + func testSK1CheckTrialOrIntroPriceEligibilityDoesntCrash() throws { + trialOrIntroPriceEligibilityChecker!.sk1CheckEligibility([]) { _ in + } + } + + func testSK1CheckTrialOrIntroPriceEligibilityFetchesAReceipt() throws { + trialOrIntroPriceEligibilityChecker!.sk1CheckEligibility([]) { _ in + } + + expect(self.receiptFetcher.receiptDataCalled) == true + } + + func testSK1EligibilityIsCalculatedFromReceiptData() throws { + let stubbedEligibility = ["product_id": IntroEligibilityStatus.eligible] + mockIntroEligibilityCalculator.stubbedCheckTrialOrIntroductoryPriceEligibilityResult = (stubbedEligibility, nil) + + var completionCalled = false + var maybeEligibilities: [String: IntroEligibility]? + trialOrIntroPriceEligibilityChecker!.sk1CheckEligibility([]) { (eligibilities) in + completionCalled = true + maybeEligibilities = eligibilities + } + + expect(completionCalled).toEventually(beTrue()) + let receivedEligibilities = try XCTUnwrap(maybeEligibilities) + expect(receivedEligibilities.count) == 1 + } + + func testSK1EligibilityIsFetchedFromBackendIfErrorCalculatingEligibility() throws { + let stubbedError = NSError(domain: RCPurchasesErrorCodeDomain, + code: ErrorCode.invalidAppUserIdError.rawValue, + userInfo: [:]) + mockIntroEligibilityCalculator.stubbedCheckTrialOrIntroductoryPriceEligibilityResult = ([:], stubbedError) + + let productId = "product_id" + let stubbedEligibility = [productId: IntroEligibility(eligibilityStatus: IntroEligibilityStatus.eligible)] + mockBackend.stubbedGetIntroEligibilityCompletionResult = (stubbedEligibility, nil) + var completionCalled = false + var maybeEligibilities: [String: IntroEligibility]? + trialOrIntroPriceEligibilityChecker!.sk1CheckEligibility([productId]) { (eligibilities) in + completionCalled = true + maybeEligibilities = eligibilities + } + + expect(completionCalled).toEventually(beTrue()) + let receivedEligibilities = try XCTUnwrap(maybeEligibilities) + expect(receivedEligibilities.count) == 1 + expect(receivedEligibilities[productId]?.status) == IntroEligibilityStatus.eligible + + expect(self.mockBackend.invokedGetIntroEligibilityCount) == 1 + } + + func testSK1ErrorFetchingFromBackendAfterErrorCalculatingEligibility() throws { + let productId = "product_id" + + let stubbedError = NSError(domain: RCPurchasesErrorCodeDomain, + code: ErrorCode.invalidAppUserIdError.rawValue, + userInfo: [:]) + mockIntroEligibilityCalculator.stubbedCheckTrialOrIntroductoryPriceEligibilityResult = ([:], stubbedError) + + mockBackend.stubbedGetIntroEligibilityCompletionResult = ([:], stubbedError) + var completionCalled = false + var maybeEligibilities: [String: IntroEligibility]? + trialOrIntroPriceEligibilityChecker!.sk1CheckEligibility([productId]) { (eligibilities) in + completionCalled = true + maybeEligibilities = eligibilities + } + + expect(completionCalled).toEventually(beTrue()) + let receivedEligibilities = try XCTUnwrap(maybeEligibilities) + expect(receivedEligibilities.count) == 1 + expect(receivedEligibilities[productId]?.status) == IntroEligibilityStatus.unknown + } + +} diff --git a/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK2Tests.swift b/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK2Tests.swift new file mode 100644 index 0000000000..ca4f88c847 --- /dev/null +++ b/StoreKitUnitTests/TrialOrIntroPriceEligibilityCheckerSK2Tests.swift @@ -0,0 +1,112 @@ +// +// 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 +// +// TrialOrIntroPriceEligibilityCheckerSK2Tests.swift +// +// Created by César de la Vega on 9/1/21. + +import Nimble +@testable import RevenueCat +import StoreKitTest +import XCTest + +// swiftlint:disable:next type_name +class TrialOrIntroPriceEligibilityCheckerSK2Tests: StoreKitConfigTestCase { + + var receiptFetcher: MockReceiptFetcher! + var trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityChecker! + var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! + var mockBackend: MockBackend! + var mockProductsManager: MockProductsManager! + var mockSystemInfo: MockSystemInfo! + + override func setUpWithError() throws { + try super.setUpWithError() + mockSystemInfo = try MockSystemInfo(platformFlavor: "xyz", + platformFlavorVersion: "123", + finishTransactions: true) + + receiptFetcher = MockReceiptFetcher(requestFetcher: MockRequestFetcher(), systemInfo: mockSystemInfo) + let mockProductsManager = PartialMockProductsManager(systemInfo: mockSystemInfo) + mockIntroEligibilityCalculator = MockIntroEligibilityCalculator(productsManager: mockProductsManager, + receiptParser: MockReceiptParser()) + mockBackend = MockBackend() + let mockOperationDispatcher = MockOperationDispatcher() + let identityManager = MockIdentityManager(mockAppUserID: "app_user") + trialOrIntroPriceEligibilityChecker = + TrialOrIntroPriceEligibilityChecker(receiptFetcher: receiptFetcher, + introEligibilityCalculator: mockIntroEligibilityCalculator, + backend: mockBackend, + identityManager: identityManager, + operationDispatcher: mockOperationDispatcher, + productsManager: mockProductsManager) + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func testSK2CheckEligibilityAsync() async throws { + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + + let products = ["product_id", + "com.revenuecat.monthly_4.99.1_week_intro", + "com.revenuecat.annual_39.99.2_week_intro", + "lifetime"] + let expected = ["product_id": IntroEligibilityStatus.unknown, + "com.revenuecat.monthly_4.99.1_week_intro": IntroEligibilityStatus.eligible, + "com.revenuecat.annual_39.99.2_week_intro": IntroEligibilityStatus.eligible, + "lifetime": IntroEligibilityStatus.unknown] + + let maybeEligibilities = await trialOrIntroPriceEligibilityChecker!.sk2CheckEligibility(products) + let receivedEligibilities = try XCTUnwrap(maybeEligibilities) + expect(receivedEligibilities.count) == expected.count + + for (product, receivedEligibility) in receivedEligibilities { + expect(receivedEligibility.status) == expected[product] + } + } + + // - Note: Xcode throws a warning about @available and #available being redundant, but they're actually necessary: + // Although the method isn't supposed to be called because of our @available marks, + // everything in this class will still be called by XCTest, and it will cause errors. + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + func testCheckEligibilityNoAsync() throws { + guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { + throw XCTSkip("Required API is not available for this test.") + } + let products = ["product_id", + "com.revenuecat.monthly_4.99.1_week_intro", + "com.revenuecat.annual_39.99.2_week_intro", + "lifetime"] + let expected = ["product_id": IntroEligibilityStatus.unknown, + "com.revenuecat.monthly_4.99.1_week_intro": IntroEligibilityStatus.eligible, + "com.revenuecat.annual_39.99.2_week_intro": IntroEligibilityStatus.eligible, + "lifetime": IntroEligibilityStatus.unknown] + + var completionCalled = false + var maybeEligibilities: [String: IntroEligibility]? + trialOrIntroPriceEligibilityChecker!.checkEligibility(productIdentifiers: products) { eligibilities in + completionCalled = true + maybeEligibilities = eligibilities + } + + expect(completionCalled).toEventually(beTrue()) + + let receivedEligibilities = try XCTUnwrap(maybeEligibilities) + expect(receivedEligibilities.count) == expected.count + + for (product, receivedEligibility) in receivedEligibilities { + expect(receivedEligibility.status) == expected[product] + } + } + +} diff --git a/StoreKitUnitTests/UnitTestsConfiguration.storekit b/StoreKitUnitTests/UnitTestsConfiguration.storekit new file mode 100644 index 0000000000..fdf36c4215 --- /dev/null +++ b/StoreKitUnitTests/UnitTestsConfiguration.storekit @@ -0,0 +1,93 @@ +{ + "identifier" : "B495CDA2", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "199.99", + "familyShareable" : true, + "internalID" : "319A9C2C", + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "lifetime", + "referenceName" : "lifetime", + "type" : "NonConsumable" + } + ], + "settings" : { + "_timeRate" : 6 + }, + "subscriptionGroups" : [ + { + "id" : "7096FF06", + "localizations" : [ + + ], + "name" : "subscription_group", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "displayPrice" : "4.99", + "familyShareable" : true, + "groupNumber" : 1, + "internalID" : "E5E51B92", + "introductoryOffer" : { + "internalID" : "D4FA46D0", + "paymentMode" : "free", + "subscriptionPeriod" : "P1W" + }, + "localizations" : [ + { + "description" : "Monthly subscription with a 1-week free trial", + "displayName" : "Monthly Free Trial", + "locale" : "en_US" + } + ], + "productID" : "com.revenuecat.monthly_4.99.1_week_intro", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "monthly free trial", + "subscriptionGroupID" : "7096FF06", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "displayPrice" : "39.99", + "familyShareable" : true, + "groupNumber" : 1, + "internalID" : "F04B58DC", + "introductoryOffer" : { + "internalID" : "0539C216", + "paymentMode" : "free", + "subscriptionPeriod" : "P2W" + }, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.revenuecat.annual_39.99.2_week_intro", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "annual free trial", + "subscriptionGroupID" : "7096FF06", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 1, + "minor" : 1 + } +} diff --git a/SwiftStyleGuide.swift b/SwiftStyleGuide.swift index da3791ff83..1215d954c2 100644 --- a/SwiftStyleGuide.swift +++ b/SwiftStyleGuide.swift @@ -163,3 +163,10 @@ guard 1 == 1, print("Universe is broken") return } + +// use _ = Task for task initialization, so that if a part of the +// Task throws, the compiler forces us to handle the exception +// More info here: https://rev.cat/catching-exceptions-from-tasks +_ = Task { + try await myMethodThatThrows() +} diff --git a/UnitTests.xctestplan b/UnitTests.xctestplan new file mode 100644 index 0000000000..e1280ac13a --- /dev/null +++ b/UnitTests.xctestplan @@ -0,0 +1,38 @@ +{ + "configurations" : [ + { + "id" : "0F20159C-1CBD-47B2-BA13-2AFF518106DE", + "name" : "Configuration 1", + "options" : { + "testTimeoutsEnabled" : true + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "targetForVariableExpansion" : { + "containerPath" : "container:RevenueCat.xcodeproj", + "identifier" : "2DEAC2D926EFE46E006914ED", + "name" : "UnitTestsHostApp" + }, + "testExecutionOrdering" : "random", + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:RevenueCat.xcodeproj", + "identifier" : "2DC5621D24EC63430031F69B", + "name" : "RevenueCatTests" + } + }, + { + "target" : { + "containerPath" : "container:RevenueCat.xcodeproj", + "identifier" : "2DAC5F7126F13C9800C5258F", + "name" : "StoreKitUnitTests" + } + } + ], + "version" : 1 +} diff --git a/UnitTestsHostApp/AppDelegate.swift b/UnitTestsHostApp/AppDelegate.swift new file mode 100644 index 0000000000..c71d067a01 --- /dev/null +++ b/UnitTestsHostApp/AppDelegate.swift @@ -0,0 +1,44 @@ +// +// 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 +// +// AppDelegate.swift +// +// Created by Andrés Boedo on 9/13/21. + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + @available(iOS 13.0, *) + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after + // application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + +} diff --git a/UnitTestsHostApp/Assets.xcassets/AccentColor.colorset/Contents.json b/UnitTestsHostApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/UnitTestsHostApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnitTestsHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/UnitTestsHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/UnitTestsHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnitTestsHostApp/Assets.xcassets/Contents.json b/UnitTestsHostApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/UnitTestsHostApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnitTestsHostApp/Base.lproj/LaunchScreen.storyboard b/UnitTestsHostApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/UnitTestsHostApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnitTestsHostApp/Base.lproj/Main.storyboard b/UnitTestsHostApp/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..25a763858e --- /dev/null +++ b/UnitTestsHostApp/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnitTestsHostApp/Info.plist b/UnitTestsHostApp/Info.plist new file mode 100644 index 0000000000..6e367de48d --- /dev/null +++ b/UnitTestsHostApp/Info.plist @@ -0,0 +1,68 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + StoreKitUnitTestsHostApp + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/UnitTestsHostApp/SceneDelegate.swift b/UnitTestsHostApp/SceneDelegate.swift new file mode 100644 index 0000000000..9f6786b782 --- /dev/null +++ b/UnitTestsHostApp/SceneDelegate.swift @@ -0,0 +1,60 @@ +// +// 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 +// +// SceneDelegate.swift +// +// Created by Andrés Boedo on 9/24/21. + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` + // to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new + // (see `application:configurationForConnectingSceneSession` instead). + // guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded + // (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + +} diff --git a/UnitTestsHostApp/ViewController.swift b/UnitTestsHostApp/ViewController.swift new file mode 100644 index 0000000000..3401d5d047 --- /dev/null +++ b/UnitTestsHostApp/ViewController.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 +// +// ViewController.swift +// +// Created by Andrés Boedo on 9/13/21. + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8931798430..a69398fa9e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -39,12 +39,16 @@ platform :ios do end desc "Runs all the tests" - lane :test do + lane :test do |options| # Scan can't target two different platforms in devices # Need to run scan multiple times scan( step_name: "scan - iPhone", - devices: ["iPhone 8", "iPhone 12"], + devices: ["iPhone 8 (14.5)", "iPhone 12 (15.0)"], + scheme: "UnitTests", + testplan: "UnitTests", + prelaunch_simulator: true, + scheme: "UnitTests", output_types: '', fail_build: false, result_bundle: true @@ -53,7 +57,12 @@ platform :ios do scan( step_name: "scan - Apple TV", + devices: ["Apple TV (15.0)"], + scheme: "RevenueCat", + testplan: "RevenueCat", + prelaunch_simulator: true, devices: ["Apple TV"], + scheme: "RevenueCat", output_types: '', fail_build: false, result_bundle: true @@ -139,7 +148,7 @@ platform :ios do check_no_github_release_exists(version_number) end - desc "tvOS, watchOS, and macOS build" + desc "build tvOS, watchOS, macOS" lane :build_tv_watch_mac do |options| check_pods carthage(command: "build", no_skip_current: true, platform: "watchOS,tvOS,Mac", use_xcframeworks: true) @@ -162,12 +171,38 @@ platform :ios do gym(export_method: "app-store") end + desc "build Swift API tester" + lane :build_swift_api_tester do + match( + type: "development", + git_url: ENV["CERTS_REPO_URL"], + storage_mode: "git", + type: "development", + app_identifier: ["com.revenuecat.SwiftAPITesterIOS"], + username: ENV["FASTLANE_USER"]) + sh('xcodebuild', '-workspace', '../APITesters/APITesters.xcworkspace', '-scheme', 'SwiftAPITesterIOS') + sh('xcodebuild', '-workspace', '../APITesters/APITesters.xcworkspace', '-scheme', 'SwiftAPITester') + end + + desc "build ObjC API tester" + lane :build_objc_api_tester do + match( + type: "development", + git_url: ENV["CERTS_REPO_URL"], + storage_mode: "git", + type: "development", + app_identifier: ["com.revenuecat.ObjCAPITesterIOS"], + username: ENV["FASTLANE_USER"]) + sh('xcodebuild', '-workspace', '../APITesters/APITesters.xcworkspace', '-scheme', 'ObjCAPITesterIOS') + sh('xcodebuild', '-workspace', '../APITesters/APITesters.xcworkspace', '-scheme', 'ObjCAPITester') + end + desc "replace API KEY for integration tests" lane :replace_api_key_integration_tests do - replace_in("REVENUECAT_API_KEY", ENV["REVENUECAT_API_KEY"], '../StoreKitTests/Constants.swift') + replace_in("REVENUECAT_API_KEY", ENV["REVENUECAT_API_KEY"], '../BackendIntegrationTests/Constants.swift') replace_in("REVENUECAT_API_KEY", ENV["REVENUECAT_API_KEY"], '../IntegrationTests/CommonFiles/RCIntegrationRunner.m') - replace_in("REVENUECAT_PROXY_URL", ENV["REVENUECAT_PROXY_URL"].to_s, '../StoreKitTests/Constants.swift', allow_empty: true) + replace_in("REVENUECAT_PROXY_URL", ENV["REVENUECAT_PROXY_URL"].to_s, '../BackendIntegrationTests/Constants.swift', allow_empty: true) replace_in("REVENUECAT_PROXY_URL", ENV["REVENUECAT_PROXY_URL"].to_s, '../IntegrationTests/CommonFiles/RCIntegrationRunner.m', allow_empty: true) end @@ -306,10 +341,10 @@ platform :ios do end - desc "Run StoreKitTests" - lane :storekit_tests do |options| + desc "Run BackendIntegrationTests" + lane :backend_integration_tests do |options| replace_api_key_integration_tests - scan(scheme: "StoreKitTests", derived_data_path: "scan_derived_data") + scan(scheme: "BackendIntegrationTests", derived_data_path: "scan_derived_data") end desc "Update swift package commit" diff --git a/fastlane/README.md b/fastlane/README.md index 236b0e8044..8a5f1a33cf 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -71,6 +71,16 @@ Run the carthage archive steps to prepare for carthage distribution fastlane ios archive ``` archive +### ios build_swift_api_tester +``` +fastlane ios build_swift_api_tester +``` +build Swift API tester +### ios build_objc_api_tester +``` +fastlane ios build_objc_api_tester +``` +build ObjC API tester ### ios replace_api_key_integration_tests ``` fastlane ios replace_api_key_integration_tests @@ -96,11 +106,11 @@ Prepare next version fastlane ios export_xcframework ``` Export XCFramework -### ios storekit_tests +### ios backend_integration_tests ``` -fastlane ios storekit_tests +fastlane ios backend_integration_tests ``` -Run StoreKitTests +Run BackendIntegrationTests ### ios update_swift_package_commit ``` fastlane ios update_swift_package_commit diff --git a/fastlane/Scanfile b/fastlane/Scanfile deleted file mode 100644 index 0547f289ab..0000000000 --- a/fastlane/Scanfile +++ /dev/null @@ -1,14 +0,0 @@ -# For more information about this configuration visit -# https://docs.fastlane.tools/actions/scan/#scanfile - -# In general, you can use the options available -# fastlane scan --help - -scheme("RevenueCat") - -# Commenting out devices in Scanfile as of fastlane 2.191.0 -# Scan cannot handle running multiple platform type (iOS and tvOS) -# in the same run but might in the future -# Future version of fastlane might allow this - -# devices(["iPhone 6", "iPhone 12", "Apple TV"])