From fd2fb2d07fc07d96fd8f2a9500db90418947b5a2 Mon Sep 17 00:00:00 2001 From: beylmk Date: Mon, 2 May 2022 09:38:55 -0700 Subject: [PATCH 01/16] [CF-106] Fetch AdServices Token (#1519) Co-authored-by: NachoSoto Co-authored-by: Josh Holtz --- Gemfile.lock | 22 +-- RevenueCat.xcodeproj/project.pbxproj | 26 +-- Sources/Attribution/AfficheClientProxy.swift | 62 ------ Sources/Attribution/AttributionFetcher.swift | 42 ++-- Sources/Attribution/AttributionNetwork.swift | 20 +- Sources/Attribution/AttributionPoster.swift | 57 ++---- .../Attribution/AttributionTypeFactory.swift | 4 - .../DocCDocumentation.docc/Purchases.md | 2 +- Sources/Identity/CustomerInfo.swift | 6 - .../Logging/Strings/AttributionStrings.swift | 10 +- Sources/Logging/Strings/StoreKitStrings.swift | 6 + Sources/Misc/Deprecations.swift | 18 ++ Sources/Networking/Backend.swift | 14 -- .../PostAttributionDataOperation.swift | 78 -------- Sources/Purchasing/Purchases.swift | 29 ++- .../StoreProductDiscount.swift | 2 +- .../AttributionDataMigrator.swift | 4 +- .../ObjCAPITester.xcodeproj/project.pbxproj | 4 + .../ObjCAPITester/RCAttributionNetworkAPI.m | 1 + .../ObjCAPITester/RCPurchasesAPI.m | 7 +- .../SwiftAPITester.xcodeproj/project.pbxproj | 4 + .../AttributionNetworkAPI.swift | 3 +- .../SwiftAPITester/PurchasesAPI.swift | 6 +- .../Attribution/AttributionPosterTests.swift | 183 ------------------ .../Mocks/MockAttributionFetcher.swift | 8 +- .../Mocks/MockAttributionTypeFactory.swift | 25 --- Tests/UnitTests/Mocks/MockBackend.swift | 21 -- .../BackendPostAttributionDataTests.swift | 42 ---- ...testPostAttributesPutsDataInDataKey.1.json | 16 -- ...testPostAttributesPutsDataInDataKey.1.json | 16 -- ...testPostAttributesPutsDataInDataKey.1.json | 16 -- ...testPostAttributesPutsDataInDataKey.1.json | 16 -- .../UnitTests/Purchasing/PurchasesTests.swift | 122 ++---------- .../PurchasesSubscriberAttributesTests.swift | 3 +- 34 files changed, 163 insertions(+), 732 deletions(-) delete mode 100644 Sources/Attribution/AfficheClientProxy.swift delete mode 100644 Sources/Networking/Operations/PostAttributionDataOperation.swift delete mode 100644 Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift delete mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json delete mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json delete mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json delete mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json diff --git a/Gemfile.lock b/Gemfile.lock index d38083ddfe..eb69684e91 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,20 +17,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.573.0) - aws-sdk-core (3.130.0) + aws-partitions (1.579.0) + aws-sdk-core (3.130.2) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.55.0) + aws-sdk-kms (1.56.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.0) + aws-sdk-s3 (1.113.1) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -116,7 +116,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.205.1) + fastlane (2.205.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -160,7 +160,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.18.0) + google-apis-androidpublisher_v3 (0.19.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) @@ -175,7 +175,7 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-playcustomapp_v1 (0.7.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.12.0) + google-apis-storage_v1 (0.13.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -183,7 +183,7 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.1) + google-cloud-storage (1.36.2) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -191,7 +191,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.2) + googleauth (1.1.3) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -234,7 +234,7 @@ GEM optparse (0.1.1) os (1.1.4) plist (3.6.0) - public_suffix (4.0.6) + public_suffix (4.0.7) rake (13.0.6) redcarpet (3.5.1) representable (3.1.1) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index c1ae0f772f..bf29ba7823 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -249,7 +249,6 @@ 5796A39427D6BD6900653165 /* BackendGetOfferingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A39327D6BD6900653165 /* BackendGetOfferingsTests.swift */; }; 5796A39627D6BDAB00653165 /* BackendPostOfferForSigningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A39527D6BDAB00653165 /* BackendPostOfferForSigningTests.swift */; }; 5796A39927D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A39827D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift */; }; - 5796A39B27D6C20A00653165 /* BackendPostAttributionDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A39A27D6C20A00653165 /* BackendPostAttributionDataTests.swift */; }; 5796A3A927D7C43500653165 /* Deprecations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A3A827D7C43500653165 /* Deprecations.swift */; }; 5796A3C027D7D64500653165 /* ResultExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5796A3BF27D7D64500653165 /* ResultExtensionsTests.swift */; }; 57A0FBF02749C0C2009E2FC3 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A0FBEF2749C0C2009E2FC3 /* Atomic.swift */; }; @@ -268,18 +267,18 @@ 57CFB96D27FE0E79002A6730 /* MockCurrentUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */; }; 57CFB98427FE2258002A6730 /* StoreKit2Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CFB98327FE2258002A6730 /* StoreKit2Setting.swift */; }; 57D04BB827D947C6006DAC06 /* HTTPResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */; }; - 57D5414227F656D9004CC35C /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5414127F656D9004CC35C /* NetworkError.swift */; }; 57D5412E27F6311C004CC35C /* OfferingsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */; }; + 57D5414227F656D9004CC35C /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5414127F656D9004CC35C /* NetworkError.swift */; }; 57DC9F4627CC2E4900DA6AF9 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */; }; 57DC9F4A27CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */; }; 57DE806D28074976008D6C6F /* Storefront.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE806C28074976008D6C6F /* Storefront.swift */; }; 57DE807128074C23008D6C6F /* SK1Storefront.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE807028074C23008D6C6F /* SK1Storefront.swift */; }; 57DE807328074C76008D6C6F /* SK2Storefront.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE807228074C76008D6C6F /* SK2Storefront.swift */; }; - 57DE80BE28077010008D6C6F /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */; }; - 57DE80BF2807705F008D6C6F /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */; }; 57DE80892807540D008D6C6F /* StorefrontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE80882807540D008D6C6F /* StorefrontTests.swift */; }; 57DE80AE28075D77008D6C6F /* OSVersionEquivalent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE80AD28075D77008D6C6F /* OSVersionEquivalent.swift */; }; 57DE80AF28075D77008D6C6F /* OSVersionEquivalent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE80AD28075D77008D6C6F /* OSVersionEquivalent.swift */; }; + 57DE80BE28077010008D6C6F /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */; }; + 57DE80BF2807705F008D6C6F /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D22BF6626F3CBFB001AE2F9 /* XCTestCase+Extensions.swift */; }; 57E0473B277260DE0082FE91 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 57E0473A277260DE0082FE91 /* SnapshotTesting */; }; 57E2230727500BB1002DB06E /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E2230627500BB1002DB06E /* AtomicTests.swift */; }; 57EAE527274324C60060EB74 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAE526274324C60060EB74 /* Lock.swift */; }; @@ -305,6 +304,7 @@ 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 */; }; + A5B6CDD8280F3843007629D5 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5B6CDD5280F3843007629D5 /* AdServices.framework */; platformFilters = (ios, maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; }; 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 */; }; @@ -333,7 +333,6 @@ B34605CA279A6E380031CA74 /* GetCustomerInfoOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605B5279A6E380031CA74 /* GetCustomerInfoOperation.swift */; }; B34605CB279A6E380031CA74 /* PostReceiptDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */; }; B34605CC279A6E380031CA74 /* LogInOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605B7279A6E380031CA74 /* LogInOperation.swift */; }; - B34605CD279A6E380031CA74 /* PostAttributionDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605B8279A6E380031CA74 /* PostAttributionDataOperation.swift */; }; B34605CE279A6E380031CA74 /* PostSubscriberAttributesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */; }; B34605CF279A6E380031CA74 /* GetOfferingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605BA279A6E380031CA74 /* GetOfferingsOperation.swift */; }; B34605D1279A6E600031CA74 /* SubscribersAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605D0279A6E600031CA74 /* SubscribersAPI.swift */; }; @@ -393,7 +392,6 @@ F5BE424226965F9F00254A30 /* ProductRequestData+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE424126965F9F00254A30 /* ProductRequestData+Initialization.swift */; }; F5BE44432698581100254A30 /* AttributionTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE44422698581100254A30 /* AttributionTypeFactory.swift */; }; F5BE444726985E7B00254A30 /* AttributionTypeFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E350420D54B99BB39448E0 /* AttributionTypeFactoryTests.swift */; }; - F5BE4479269E4A4C00254A30 /* AfficheClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE4478269E4A4C00254A30 /* AfficheClientProxy.swift */; }; F5BE447B269E4A7500254A30 /* TrackingManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */; }; F5BE447D269E4ADB00254A30 /* ASIdManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE447C269E4ADB00254A30 /* ASIdManagerProxy.swift */; }; F5C0196926E880800005D61E /* StoreKitStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C0196826E880800005D61E /* StoreKitStrings.swift */; }; @@ -687,7 +685,6 @@ 5796A39527D6BDAB00653165 /* BackendPostOfferForSigningTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostOfferForSigningTests.swift; sourceTree = ""; }; 5796A39727D6C07D00653165 /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; 5796A39827D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostSubscriberAttributesTests.swift; sourceTree = ""; }; - 5796A39A27D6C20A00653165 /* BackendPostAttributionDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostAttributionDataTests.swift; sourceTree = ""; }; 5796A3A827D7C43500653165 /* Deprecations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecations.swift; sourceTree = ""; }; 5796A3BF27D7D64500653165 /* ResultExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultExtensionsTests.swift; sourceTree = ""; }; 57A0FBEF2749C0C2009E2FC3 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; @@ -704,8 +701,8 @@ 57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCurrentUserProvider.swift; sourceTree = ""; }; 57CFB98327FE2258002A6730 /* StoreKit2Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2Setting.swift; sourceTree = ""; }; 57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponseTests.swift; sourceTree = ""; }; - 57D5414127F656D9004CC35C /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsResponse.swift; sourceTree = ""; }; + 57D5414127F656D9004CC35C /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; 57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCodeTests.swift; sourceTree = ""; }; 57DE806C28074976008D6C6F /* Storefront.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storefront.swift; sourceTree = ""; }; @@ -736,6 +733,7 @@ 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 = ""; }; + A5B6CDD5280F3843007629D5 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/AdServices.framework; sourceTree = DEVELOPER_DIR; }; 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 = ""; }; B302206D2728B798008F1A0D /* BackendErrorStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendErrorStrings.swift; sourceTree = ""; }; @@ -759,7 +757,6 @@ B34605B5279A6E380031CA74 /* GetCustomerInfoOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetCustomerInfoOperation.swift; sourceTree = ""; }; B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostReceiptDataOperation.swift; sourceTree = ""; }; B34605B7279A6E380031CA74 /* LogInOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogInOperation.swift; sourceTree = ""; }; - B34605B8279A6E380031CA74 /* PostAttributionDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostAttributionDataOperation.swift; sourceTree = ""; }; B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostSubscriberAttributesOperation.swift; sourceTree = ""; }; B34605BA279A6E380031CA74 /* GetOfferingsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetOfferingsOperation.swift; sourceTree = ""; }; B34605D0279A6E600031CA74 /* SubscribersAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribersAPI.swift; sourceTree = ""; }; @@ -818,7 +815,6 @@ F5BE424126965F9F00254A30 /* ProductRequestData+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductRequestData+Initialization.swift"; sourceTree = ""; }; F5BE4244269676E200254A30 /* StoreKitRequestFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreKitRequestFetcherTests.swift; sourceTree = ""; }; F5BE44422698581100254A30 /* AttributionTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionTypeFactory.swift; sourceTree = ""; }; - F5BE4478269E4A4C00254A30 /* AfficheClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AfficheClientProxy.swift; sourceTree = ""; }; F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingManagerProxy.swift; sourceTree = ""; }; F5BE447C269E4ADB00254A30 /* ASIdManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASIdManagerProxy.swift; sourceTree = ""; }; F5C0196826E880800005D61E /* StoreKitStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitStrings.swift; sourceTree = ""; }; @@ -844,6 +840,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A5B6CDD8280F3843007629D5 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1328,6 +1325,7 @@ 3530C18722653E8F00D6DF52 /* Frameworks */ = { isa = PBXGroup; children = ( + A5B6CDD5280F3843007629D5 /* AdServices.framework */, B36824BD268FBC5B00957E4C /* XCTest.framework */, 2DE20B9126409ECF004C597D /* StoreKit.framework */, 2DE20B7526408806004C597D /* StoreKitTest.framework */, @@ -1560,7 +1558,6 @@ 5796A38F27D6BCD100653165 /* BackendGetIntroEligibilityTests.swift */, 5796A39327D6BD6900653165 /* BackendGetOfferingsTests.swift */, 5796A38B27D6BA1600653165 /* BackendLoginTests.swift */, - 5796A39A27D6C20A00653165 /* BackendPostAttributionDataTests.swift */, 5796A39527D6BDAB00653165 /* BackendPostOfferForSigningTests.swift */, 5796A39827D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift */, 5796A38727D6B85900653165 /* BackendPostReceiptDataTests.swift */, @@ -1627,7 +1624,6 @@ B34605B4279A6E380031CA74 /* GetIntroEligibilityOperation.swift */, B34605BA279A6E380031CA74 /* GetOfferingsOperation.swift */, B34605B7279A6E380031CA74 /* LogInOperation.swift */, - B34605B8279A6E380031CA74 /* PostAttributionDataOperation.swift */, B34605AD279A6E380031CA74 /* PostOfferForSigningOperation.swift */, B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */, B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */, @@ -1704,7 +1700,6 @@ isa = PBXGroup; children = ( B39E8119268E849900D31189 /* AttributionNetwork.swift */, - F5BE4478269E4A4C00254A30 /* AfficheClientProxy.swift */, F5BE447C269E4ADB00254A30 /* ASIdManagerProxy.swift */, 37E35CD16BB73BB091E64D9A /* AttributionData.swift */, 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */, @@ -2168,7 +2163,6 @@ B39E811D268E887500D31189 /* SubscriberAttribute.swift in Sources */, A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */, B34605C0279A6E380031CA74 /* CustomerInfoCallback.swift in Sources */, - F5BE4479269E4A4C00254A30 /* AfficheClientProxy.swift in Sources */, B33CEAA0268CDCC9008A3144 /* ISOPeriodFormatter.swift in Sources */, 2DDF41A324F6F331005BC22D /* ReceiptParser.swift in Sources */, 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */, @@ -2273,7 +2267,6 @@ F5714EA526D6C24D00635477 /* JSONDecoder+Extensions.swift in Sources */, B302206A27271BCB008F1A0D /* Decoder+Extensions.swift in Sources */, B3766F1E26BDA95100141450 /* IntroEligibilityResponse.swift in Sources */, - B34605CD279A6E380031CA74 /* PostAttributionDataOperation.swift in Sources */, B34605C1279A6E380031CA74 /* NetworkOperation.swift in Sources */, 35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */, 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */, @@ -2303,7 +2296,6 @@ 2D4D6AF524F717B800B656BE /* ContainerFactory.swift in Sources */, 351B514726D44A0D00BD2BD7 /* MockSystemInfo.swift in Sources */, B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */, - 5796A39B27D6C20A00653165 /* BackendPostAttributionDataTests.swift in Sources */, 2DDF41CA24F6F4C3005BC22D /* ArraySlice_UInt8+ExtensionsTests.swift in Sources */, 2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */, 351B514D26D44A8600BD2BD7 /* MockHTTPClient.swift in Sources */, diff --git a/Sources/Attribution/AfficheClientProxy.swift b/Sources/Attribution/AfficheClientProxy.swift deleted file mode 100644 index 81aff940d5..0000000000 --- a/Sources/Attribution/AfficheClientProxy.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright RevenueCat Inc. All Rights Reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// AfficheClientProxy.swift -// -// Created by Juanpe Catalán on 14/7/21. -// - -import Foundation - -typealias AttributionDetailsBlock = ([String: NSObject]?, Error?) -> Void - -// We need this class to avoid Kid apps being rejected for getting idfa. It seems like App -// Review uses some grep to find the class names, so we ended up creating a fake class that -// exposes the same methods we're looking for in ADClient to call the same methods and mangling -// the class names. So that Apple can't find them during the review, but we can still access them on runtime. -// You can see the class here: https://rev.cat/fake-affiche-client -class FakeAfficheClient: NSObject { - - // We need this method to be available as an optional implicitly unwrapped method for `AnyClass`. - @objc static func sharedClient() -> FakeAfficheClient { - FakeAfficheClient() - } - - // We need this method to be available as an optional implicitly unwrapped method for `AnyClass`. - @objc(requestAttributionDetailsWithBlock:) - func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { - Logger.warn(Strings.attribution.apple_affiche_framework_present_but_couldnt_call_request_attribution_details) - } - -} - -class AfficheClientProxy { - - private static let mangledClassName = "NQPyvrag" - - static var afficheClientClass: AnyClass? { - NSClassFromString(Self.mangledClassName.rot13()) - } - - func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { - let client: AnyObject - if let klass = Self.afficheClientClass, let clientClass = klass as AnyObject as? NSObjectProtocol { - // This looks strange, but #selector() does fun things to create a selector. If the selector for the given - // function matches the selector on another class, it can be used in place. Results: - // If ADClient class is instantiated above, then +sharedClient selector is performed even though you can see - // that we're using #selector(FakeAfficheClient.sharedClient) to instantiate a Selector object. - client = clientClass.perform(#selector(FakeAfficheClient.sharedClient)).takeUnretainedValue() - } else { - client = FakeAfficheClient.sharedClient() - } - - client.requestAttributionDetails(completionHandler) - } - -} diff --git a/Sources/Attribution/AttributionFetcher.swift b/Sources/Attribution/AttributionFetcher.swift index 155166c333..2385e5615e 100644 --- a/Sources/Attribution/AttributionFetcher.swift +++ b/Sources/Attribution/AttributionFetcher.swift @@ -19,10 +19,16 @@ import UIKit import WatchKit #endif +#if canImport(AdServices) +import AdServices +#endif + enum AttributionFetcherError: Error { case identifierForAdvertiserUnavailableForPlatform case identifierForAdvertiserFrameworksUnavailable + case adServicesNotAvailable + case adServicesTokenFetchError } @@ -67,35 +73,23 @@ class AttributionFetcher { return nil } - func afficheClientAttributionDetails(completion: @escaping ([String: NSObject]?, Error?) -> Void) { - // Should match available platforms in - // https://developer.apple.com/documentation/iad/adclient?language=swift -#if os(iOS) - guard let afficheClientProxy = attributionFactory.afficheClientProxy() else { - Logger.warn(Strings.attribution.search_ads_attribution_cancelled_missing_ad_framework) - completion(nil, AttributionFetcherError.identifierForAdvertiserFrameworksUnavailable) - return + // should match OS availability in https://developer.apple.com/documentation/ad_services + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + var adServicesToken: String? { +#if canImport(AdServices) + do { + return try AAAttribution.attributionToken() + } catch { + let message = Strings.attribution.adservices_token_fetch_failed(error: error) + Logger.appleWarning(message) + return nil } - afficheClientProxy.requestAttributionDetails(completion) #else - completion(nil, AttributionFetcherError.identifierForAdvertiserUnavailableForPlatform) + Logger.warn(Strings.attribution.adservices_not_supported) + return nil #endif } - var isAuthorizedToPostSearchAds: Bool { - // Should match platforms that require permissions detailed in - // https://developer.apple.com/app-store/user-privacy-and-data-use/ - if !appTrackingTransparencyRequired { - return true - } - - if #available(iOS 14.0.0, tvOS 14.0.0, *) { - return isAuthorizedToPostSearchAdsInATTRequiredOS - } - - return true - } - var authorizationStatus: FakeTrackingManagerAuthorizationStatus { // should match OS availability here: https://rev.cat/app-tracking-transparency guard #available(iOS 14.0.0, tvOS 14.0.0, macOS 11.0.0, *) else { diff --git a/Sources/Attribution/AttributionNetwork.swift b/Sources/Attribution/AttributionNetwork.swift index 04ccd0d52e..dadb146d71 100644 --- a/Sources/Attribution/AttributionNetwork.swift +++ b/Sources/Attribution/AttributionNetwork.swift @@ -22,37 +22,43 @@ import Foundation /** Apple's search ads */ - case appleSearchAds + @available(*, deprecated, message: "use adServices") + case appleSearchAds = 0 /** Adjust https://www.adjust.com/ */ - case adjust + case adjust = 1 /** AppsFlyer https://www.appsflyer.com/ */ - case appsFlyer + case appsFlyer = 2 /** Branch https://www.branch.io/ */ - case branch + case branch = 3 /** Tenjin https://www.tenjin.io/ */ - case tenjin + case tenjin = 4 /** Facebook https://developers.facebook.com/ */ - case facebook + case facebook = 5 /** mParticle https://www.mparticle.com/ */ - case mParticle + case mParticle = 6 + + /** + AdServices token + */ + case adServices = 7 } diff --git a/Sources/Attribution/AttributionPoster.swift b/Sources/Attribution/AttributionPoster.swift index 40df8469b5..48cb037783 100644 --- a/Sources/Attribution/AttributionPoster.swift +++ b/Sources/Attribution/AttributionPoster.swift @@ -35,7 +35,6 @@ class AttributionPoster { self.subscriberAttributesManager = subscriberAttributesManager } - // swiftlint:disable:next function_body_length func post(attributionData data: [String: Any], fromNetwork network: AttributionNetwork, networkUserId: String?) { @@ -88,46 +87,27 @@ class AttributionPoster { } if !newData.isEmpty { - if network == .appleSearchAds { - postSearchAds(newData: newData, - network: network, - appUserID: currentAppUserID, - newDictToCache: newDictToCache) - } else { - postSubscriberAttributes(newData: newData, - network: network, - appUserID: currentAppUserID, - newDictToCache: newDictToCache) - } + postSubscriberAttributes(newData: newData, + network: network, + appUserID: currentAppUserID, + newDictToCache: newDictToCache) } } - func postAppleSearchAdsAttributionIfNeeded() { - guard attributionFetcher.isAuthorizedToPostSearchAds else { + // should match OS availability in https://developer.apple.com/documentation/ad_services + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + func postAdServicesTokenIfNeeded() { + let latestTokenSent = latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) + guard latestTokenSent == nil else { return } - let latestIdsSent = latestNetworkIdAndAdvertisingIdentifierSent(network: .appleSearchAds) - guard latestIdsSent == nil else { + guard let attributionToken = attributionFetcher.adServicesToken else { return } - attributionFetcher.afficheClientAttributionDetails { attributionDetails, error in - guard let attributionDetails = attributionDetails, - error == nil else { - return - } - - let attributionDetailsValues = Array(attributionDetails.values) - let firstAttributionDict = attributionDetailsValues.first as? [String: NSObject] - - guard let hasIad = firstAttributionDict?["iad-attribution"] as? NSNumber, - hasIad.boolValue == true else { - return - } - - self.post(attributionData: attributionDetails, fromNetwork: .appleSearchAds, networkUserId: nil) - } + Logger.debug("Logging attribution token for now to avoid lint warning: \(attributionToken)") + // post } func postPostponedAttributionDataIfNeeded() { @@ -162,19 +142,6 @@ class AttributionPoster { return cachedDict[networkID] } - private func postSearchAds(newData: [String: Any], - network: AttributionNetwork, - appUserID: String, - newDictToCache: [String: String]) { - backend.post(attributionData: newData, network: network, appUserID: appUserID) { error in - guard error == nil else { - return - } - - self.deviceCache.set(latestNetworkAndAdvertisingIdsSent: newDictToCache, appUserID: appUserID) - } - } - private func postSubscriberAttributes(newData: [String: Any], network: AttributionNetwork, appUserID: String, diff --git a/Sources/Attribution/AttributionTypeFactory.swift b/Sources/Attribution/AttributionTypeFactory.swift index 70717f241a..ceb0697559 100644 --- a/Sources/Attribution/AttributionTypeFactory.swift +++ b/Sources/Attribution/AttributionTypeFactory.swift @@ -16,10 +16,6 @@ import Foundation class AttributionTypeFactory { - func afficheClientProxy() -> AfficheClientProxy? { - return AfficheClientProxy.afficheClientClass == nil ? nil : AfficheClientProxy() - } - func atFollowingProxy() -> TrackingManagerProxy? { return TrackingManagerProxy.trackingClass == nil ? nil : TrackingManagerProxy() } diff --git a/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md b/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md index ceb1c236af..6aadacb578 100644 --- a/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md +++ b/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md @@ -110,7 +110,7 @@ Most features require configuring the SDK before using it. - ``Purchases/finishTransactions`` - ``Purchases/invalidateCustomerInfoCache()`` - ``Purchases/forceUniversalAppStore`` -- ``Purchases/automaticAppleSearchAdsAttributionCollection`` +- ``Purchases/automaticAdServicesAttributionTokenCollection`` - ``Purchases/proxyURL`` - ``Purchases/verboseLogs`` - ``Purchases/verboseLogHandler`` diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index 63c578404d..b9c81dacdc 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -43,12 +43,6 @@ import Foundation return mostRecentDate } - /// Returns all product IDs of the non-subscription purchases a user has made. - @available(*, deprecated, message: "use nonSubscriptionTransactions") - @objc public var nonConsumablePurchases: Set { - Set(self.nonSubscriptionTransactions.map { $0.productIdentifier }) - } - /** * Returns all the non-subscription purchases a user has made. * The purchases are ordered by purchase date in ascending order. diff --git a/Sources/Logging/Strings/AttributionStrings.swift b/Sources/Logging/Strings/AttributionStrings.swift index 66fb130bc0..b1e511427d 100644 --- a/Sources/Logging/Strings/AttributionStrings.swift +++ b/Sources/Logging/Strings/AttributionStrings.swift @@ -38,7 +38,8 @@ enum AttributionStrings { case unsynced_attributes(unsyncedAttributes: SubscriberAttributeDict) case attribute_set_locally(attribute: String) case missing_advertiser_identifiers - case unknown_sk2_product_discount_type(rawValue: String) + case adservices_not_supported + case adservices_token_fetch_failed(error: Error) } @@ -115,9 +116,12 @@ extension AttributionStrings: CustomStringConvertible { case .missing_advertiser_identifiers: return "Attribution error: identifierForAdvertisers is missing" - case .unknown_sk2_product_discount_type(let rawValue): - return "Failed to create StoreProductDiscount.DiscountType with unknown value: \(rawValue)" + case .adservices_not_supported: + return "Tried to fetch AdServices attribution token on device without " + + "AdServices support." + case .adservices_token_fetch_failed(let error): + return "Fetching AdServices attribution token failed with error: \(error.localizedDescription)" } } diff --git a/Sources/Logging/Strings/StoreKitStrings.swift b/Sources/Logging/Strings/StoreKitStrings.swift index 38a9bc927f..7ab0b6a53e 100644 --- a/Sources/Logging/Strings/StoreKitStrings.swift +++ b/Sources/Logging/Strings/StoreKitStrings.swift @@ -34,6 +34,8 @@ enum StoreKitStrings { case sk1_no_known_product_type + case unknown_sk2_product_discount_type(rawValue: String) + } extension StoreKitStrings: CustomStringConvertible { @@ -71,6 +73,10 @@ extension StoreKitStrings: CustomStringConvertible { case .sk1_no_known_product_type: return "This StoreProduct represents an SK1 product, the type of product cannot be determined, " + "the value will be undefined. Use `StoreProduct.productCategory` instead." + + case .unknown_sk2_product_discount_type(let rawValue): + return "Failed to create StoreProductDiscount.DiscountType with unknown value: \(rawValue)" + } } diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index 6014cc6614..f039b80c05 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -56,9 +56,16 @@ public extension Purchases { return await eligiblePromotionalOffers(forProduct: product) } + /** + * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. + */ + @available(*, deprecated, message: "Use Purchases.automaticAdServicesAttributionTokenCollection instead") + @objc static var automaticAppleSearchAdsAttributionCollection: Bool = false + } public extension StoreProduct { + @available(iOS, introduced: 13.0, deprecated, renamed: "eligiblePromotionalOffers()") @available(tvOS, introduced: 13.0, deprecated, renamed: "eligiblePromotionalOffers()") @available(watchOS, introduced: 6.2, deprecated, renamed: "eligiblePromotionalOffers()") @@ -67,4 +74,15 @@ public extension StoreProduct { func getEligiblePromotionalOffers() async -> [PromotionalOffer] { return await self.eligiblePromotionalOffers() } + +} + +extension CustomerInfo { + + /// Returns all product IDs of the non-subscription purchases a user has made. + @available(*, deprecated, message: "use nonSubscriptionTransactions") + @objc public var nonConsumablePurchases: Set { + return Set(self.nonSubscriptionTransactions.map { $0.productIdentifier }) + } + } diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index ba816e3c25..7598c2357b 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -131,20 +131,6 @@ class Backend { self.operationQueue.addOperation(postOfferForSigningOperation) } - func post(attributionData: [String: Any], - network: AttributionNetwork, - appUserID: String, - completion: SimpleResponseHandler?) { - let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.httpClient, - authHeaders: self.authHeaders, - appUserID: appUserID) - let postAttributionDataOperation = PostAttributionDataOperation(configuration: config, - attributionData: attributionData, - network: network, - responseHandler: completion) - self.operationQueue.addOperation(postAttributionDataOperation) - } - func logIn(currentAppUserID: String, newAppUserID: String, completion: @escaping LogInResponseHandler) { diff --git a/Sources/Networking/Operations/PostAttributionDataOperation.swift b/Sources/Networking/Operations/PostAttributionDataOperation.swift deleted file mode 100644 index 70468ea88a..0000000000 --- a/Sources/Networking/Operations/PostAttributionDataOperation.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright RevenueCat Inc. All Rights Reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// PostAttributionDataOperation.swift -// -// Created by Joshua Liebowitz on 11/19/21. - -import Foundation - -class PostAttributionDataOperation: NetworkOperation { - - private let configuration: UserSpecificConfiguration - private let attributionData: [String: Any] - private let network: AttributionNetwork - private let responseHandler: Backend.SimpleResponseHandler? - - init(configuration: UserSpecificConfiguration, - attributionData: [String: Any], - network: AttributionNetwork, - responseHandler: Backend.SimpleResponseHandler?) { - self.attributionData = attributionData - self.network = network - self.configuration = configuration - self.responseHandler = responseHandler - - super.init(configuration: configuration) - } - - override func begin(completion: @escaping () -> Void) { - self.post(completion: completion) - } - - private func post(completion: @escaping () -> Void) { - guard let appUserID = try? self.configuration.appUserID.escapedOrError() else { - self.responseHandler?(.missingAppUserID()) - completion() - - return - } - - let request = HTTPRequest(method: .post(Body(network: self.network, attributionData: self.attributionData)), - path: .postAttributionData(appUserID: appUserID)) - - self.httpClient.perform( - request, - authHeaders: self.authHeaders - ) { (response: HTTPResponse.Result) in - defer { - completion() - } - - self.responseHandler?(response.error.map(BackendError.networkError)) - } - } - -} - -private extension PostAttributionDataOperation { - - struct Body: Encodable { - - let network: AttributionNetwork - let data: AnyEncodable - - init(network: AttributionNetwork, attributionData: [String: Any]) { - self.network = network - self.data = AnyEncodable(attributionData) - } - - } - -} diff --git a/Sources/Purchasing/Purchases.swift b/Sources/Purchasing/Purchases.swift index d921d078b6..9141bf33f1 100644 --- a/Sources/Purchasing/Purchases.swift +++ b/Sources/Purchasing/Purchases.swift @@ -91,9 +91,12 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void private let operationDispatcher: OperationDispatcher /** - * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. + * Enable automatic collection of AdServices attribution token. Defaults to `false`. + * + * Should match OS availability in https://developer.apple.com/documentation/ad_services */ - @objc public static var automaticAppleSearchAdsAttributionCollection: Bool = false + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @objc public static var automaticAdServicesAttributionTokenCollection: Bool = false /** * Used to set the log level. Useful for debugging issues with the lovely team @RevenueCat. @@ -415,7 +418,11 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void } subscribeToAppStateNotifications() attributionPoster.postPostponedAttributionDataIfNeeded() - postAppleSearchAddsAttributionCollectionIfNeeded() + + // should match OS availability in https://developer.apple.com/documentation/ad_services + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + postAdServicesTokenIfNeeded() + } self.customerInfoObservationDisposable = customerInfoManager.monitorChanges { [weak self] customerInfo in guard let self = self else { return } @@ -438,7 +445,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void storeKitWrapper.delegate = nil customerInfoObservationDisposable?() privateDelegate = nil - Self.automaticAppleSearchAdsAttributionCollection = false + Self.proxyURL = nil } @@ -745,11 +752,13 @@ extension Purchases { attributionPoster.post(attributionData: data, fromNetwork: network, networkUserId: networkUserId) } - private func postAppleSearchAddsAttributionCollectionIfNeeded() { - guard Self.automaticAppleSearchAdsAttributionCollection else { + // should match OS availability in https://developer.apple.com/documentation/ad_services + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + private func postAdServicesTokenIfNeeded() { + guard Self.automaticAdServicesAttributionTokenCollection else { return } - attributionPoster.postAppleSearchAdsAttributionIfNeeded() + attributionPoster.postAdServicesTokenIfNeeded() } } @@ -1967,7 +1976,11 @@ private extension Purchases { Logger.debug(Strings.configure.application_active) updateAllCachesIfNeeded() dispatchSyncSubscriberAttributesIfNeeded() - postAppleSearchAddsAttributionCollectionIfNeeded() + + // should match OS availability in https://developer.apple.com/documentation/ad_services + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + postAdServicesTokenIfNeeded() + } } @objc func applicationWillResignActive(notification: Notification) { diff --git a/Sources/Purchasing/StoreKitAbstractions/StoreProductDiscount.swift b/Sources/Purchasing/StoreKitAbstractions/StoreProductDiscount.swift index 0b4f58a2d3..043ddcfa9c 100644 --- a/Sources/Purchasing/StoreKitAbstractions/StoreProductDiscount.swift +++ b/Sources/Purchasing/StoreKitAbstractions/StoreProductDiscount.swift @@ -259,7 +259,7 @@ extension StoreProductDiscount.DiscountType { case SK2ProductDiscount.OfferType.promotional: return .promotional default: - Logger.warn(Strings.attribution.unknown_sk2_product_discount_type(rawValue: sk2Discount.type.rawValue)) + Logger.warn(Strings.storeKit.unknown_sk2_product_discount_type(rawValue: sk2Discount.type.rawValue)) return nil } } diff --git a/Sources/SubscriberAttributes/AttributionDataMigrator.swift b/Sources/SubscriberAttributes/AttributionDataMigrator.swift index 1dfec879fb..d8eba5b47c 100644 --- a/Sources/SubscriberAttributes/AttributionDataMigrator.swift +++ b/Sources/SubscriberAttributes/AttributionDataMigrator.swift @@ -61,8 +61,8 @@ private extension AttributionDataMigrator { networkSpecificSubscriberAttributes = [:] case .mParticle: networkSpecificSubscriberAttributes = convertMParticleAttribution(attributionData) - case .none, .appleSearchAds: - // Apple Search Ads uses standard attribution system + case .none, .appleSearchAds, .adServices: + // Apple Search Ads & AdServices use standard attribution system networkSpecificSubscriberAttributes = [:] } diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj index e817a1e119..f1f1c0598a 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2C396F5E281C64B700669657 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C396F5D281C64B700669657 /* AdServices.framework */; }; 2DD77909270E23870079CBD4 /* RCAttributionNetworkAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EC26EBE84F007DDB75 /* RCAttributionNetworkAPI.m */; }; 2DD7790B270E23870079CBD4 /* RCCustomerInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F026EBE84F007DDB75 /* RCCustomerInfoAPI.m */; }; 2DD7790C270E23870079CBD4 /* RCPurchasesErrorCodeAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EE26EBE84F007DDB75 /* RCPurchasesErrorCodeAPI.m */; }; @@ -45,6 +46,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2C396F5D281C64B700669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; 2DD778F5270E235B0079CBD4 /* ObjCAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ObjCAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5738F426278672070096D623 /* RCStoreProductDiscountAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCStoreProductDiscountAPI.h; sourceTree = ""; }; 5738F427278672070096D623 /* RCStoreProductDiscountAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCStoreProductDiscountAPI.m; sourceTree = ""; }; @@ -95,6 +97,7 @@ buildActionMask = 2147483647; files = ( 575885A42748274E00CA2169 /* RevenueCat.framework in Frameworks */, + 2C396F5E281C64B700669657 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -165,6 +168,7 @@ A52A8A3A26EC2AEC00F06846 /* Frameworks */ = { isa = PBXGroup; children = ( + 2C396F5D281C64B700669657 /* AdServices.framework */, 575885A32748274E00CA2169 /* RevenueCat.framework */, 5758859F2748274600CA2169 /* RevenueCat.framework */, 57588593274826DF00CA2169 /* RevenueCat.framework */, diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionNetworkAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionNetworkAPI.m index 9a846d310f..4c31ce1ba2 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionNetworkAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionNetworkAPI.m @@ -15,6 +15,7 @@ @implementation RCAttributionNetworkAPI + (void)checkEnums { RCAttributionNetwork network = RCAttributionNetworkAdjust; switch(network) { + case RCAttributionNetworkAdServices: case RCAttributionNetworkAppleSearchAds: case RCAttributionNetworkAdjust: case RCAttributionNetworkAppsFlyer: diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index ca9733f93b..d35a48c4c8 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -16,6 +16,7 @@ @implementation RCPurchasesAPI NSString *version; BOOL automaticAppleSearchAdsAttributionCollection; +BOOL automaticAdServicesAttributionTokenCollection; BOOL debugLogsEnabled; RCLogLevel logLevel; NSURL *proxyURL; @@ -72,9 +73,13 @@ + (void)checkAPI { [RCPurchases addAttributionData:@{} fromNetwork:RCAttributionNetworkBranch]; [RCPurchases addAttributionData:@{} fromNetwork:RCAttributionNetworkBranch forNetworkUserId:@""]; [RCPurchases addAttributionData:@{} fromNetwork:RCAttributionNetworkBranch forNetworkUserId:nil]; - + + // should have deprecation warning: + // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use Purchases.automaticAdServicesAttributionTokenCollection instead automaticAppleSearchAdsAttributionCollection = [RCPurchases automaticAppleSearchAdsAttributionCollection]; + automaticAdServicesAttributionTokenCollection = [RCPurchases automaticAdServicesAttributionTokenCollection]; + // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead debugLogsEnabled = [RCPurchases debugLogsEnabled]; diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj index 52fbce1227..4b83f198c2 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2C396F5C281C64AF00669657 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C396F5B281C64AF00669657 /* AdServices.framework */; }; 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 */; }; @@ -45,6 +46,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2C396F5B281C64AF00669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; 2DD778D0270E233F0079CBD4 /* SwiftAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductDiscountAPI.swift; sourceTree = ""; }; 5738F429278673A80096D623 /* SubscriptionPeriodAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPeriodAPI.swift; sourceTree = ""; }; @@ -75,6 +77,7 @@ buildActionMask = 2147483647; files = ( 5758859C2748272A00CA2169 /* RevenueCat.framework in Frameworks */, + 2C396F5C281C64AF00669657 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,6 +87,7 @@ 2D6F3871270E58DB002C9987 /* Frameworks */ = { isa = PBXGroup; children = ( + 2C396F5B281C64AF00669657 /* AdServices.framework */, 5758859B2748272A00CA2169 /* RevenueCat.framework */, 575885972748271100CA2169 /* RevenueCat.framework */, ); diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionNetworkAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionNetworkAPI.swift index 3852873be2..b948ed9a93 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionNetworkAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionNetworkAPI.swift @@ -23,7 +23,8 @@ func checkAttributionNetworkEnums() { .branch, .tenjin, .facebook, - .mParticle: + .mParticle, + .adServices: print(aNetwork!) @unknown default: fatalError() diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index ad10bbdbdf..ec10e9b55e 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -83,6 +83,8 @@ private func checkStaticMethods() { Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: "") Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: nil) + // should have deprecation warning 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use + // Purchases.automaticAdServicesAttributionTokenCollection instead let automaticAppleSearchAdsAttributionCollection: Bool = Purchases.automaticAppleSearchAdsAttributionCollection // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead let debugLogsEnabled: Bool = Purchases.debugLogsEnabled @@ -92,9 +94,11 @@ private func checkStaticMethods() { let simulatesAskToBuyInSandbox: Bool = Purchases.simulatesAskToBuyInSandbox let sharedPurchases: Purchases = Purchases.shared let isPurchasesConfigured: Bool = Purchases.isConfigured + let automaticAdServicesAttributionTokenCollection: Bool = Purchases.automaticAdServicesAttributionTokenCollection print(canI, version, automaticAppleSearchAdsAttributionCollection, debugLogsEnabled, logLevel, proxyUrl!, - forceUniversalAppStore, simulatesAskToBuyInSandbox, sharedPurchases, isPurchasesConfigured) + forceUniversalAppStore, simulatesAskToBuyInSandbox, sharedPurchases, isPurchasesConfigured, + automaticAdServicesAttributionTokenCollection) } private func checkPurchasesPurchasingAPI(purchases: Purchases) { diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index d5a03355cf..1241bfac01 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -57,7 +57,6 @@ class AttributionPosterTests: XCTestCase { attributionFetcher: attributionFetcher, subscriberAttributesManager: subscriberAttributesManager) resetAttributionStaticProperties() - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) } private func resetAttributionStaticProperties() { @@ -65,9 +64,7 @@ class AttributionPosterTests: XCTestCase { MockTrackingManagerProxy.mockAuthorizationStatus = .authorized } - MockAttributionTypeFactory.shouldReturnAdClientProxy = true MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - MockAdClientProxy.requestAttributionDetailsCallCount = 0 } override func tearDown() { @@ -79,223 +76,43 @@ class AttributionPosterTests: XCTestCase { func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 } - func testPostAppleSearchAdsAttributionDataSkipsIfAlreadySent() { - let userID = "userID" - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) - - attributionPoster.post(attributionData: ["something": "here"], - fromNetwork: .appleSearchAds, - networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - - attributionPoster.post(attributionData: ["something": "else"], - fromNetwork: .appleSearchAds, - networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - - } - func testPostAttributionDataDoesntSkipIfNetworkChanged() { let userID = "userID" - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .facebook, networkUserId: userID) - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } func testPostAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) - attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: "attributionUser1") - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: "attributionUser2") - - expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } - func testPostAppleSearchAdsAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { - backend.stubbedPostAttributionDataCompletionResult = (nil, ()) - - attributionPoster.post(attributionData: ["something": "here"], - fromNetwork: .appleSearchAds, - networkUserId: "attributionUser1") - expect(self.backend.invokedPostAttributionDataCount) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - - attributionPoster.post(attributionData: ["something": "else"], - fromNetwork: .appleSearchAds, - networkUserId: "attributionUser2") - - expect(self.backend.invokedPostAttributionDataCount) == 2 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfATTFrameworkNotIncludedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = false - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfIAdFrameworkNotIncluded() { - MockAttributionTypeFactory.shouldReturnAdClientProxy = false - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - } - - // `MockTrackingManagerProxy.mockAuthorizationStatus isn't available on tvOS - #if os(iOS) - - func testPostAppleSearchAdsAttributionIfNeededPostsIfATTFrameworkNotIncludedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = false - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - } - - func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true - - MockTrackingManagerProxy.mockAuthorizationStatus = .authorized - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - } - - func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false - MockTrackingManagerProxy.mockAuthorizationStatus = .authorized - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - } - - func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthNotDeterminedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false - MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfAuthNotDeterminedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true - - MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false - MockTrackingManagerProxy.mockAuthorizationStatus = .denied - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true - MockTrackingManagerProxy.mockAuthorizationStatus = .denied - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - } - - func testPostAppleSearchAdsAttributionIfNeededSkipsIfAlreadySent() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - - MockTrackingManagerProxy.mockAuthorizationStatus = .authorized - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() - - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 - } - - #endif } diff --git a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift index 636ee45921..3276ac4d08 100644 --- a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift +++ b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift @@ -15,9 +15,9 @@ class MockAttributionFetcher: AttributionFetcher { return "rc_idfv" } - override func afficheClientAttributionDetails( - completion completionHandler: @escaping ([String: NSObject]?, Error?) -> Void - ) { - completionHandler(["Version3.1": ["iad-campaign-id": 15292426, "iad-attribution": true] as NSObject], nil) + var adServicesTokenCollectionCalled = false + override var adServicesToken: String? { + adServicesTokenCollectionCalled = true + return nil } } diff --git a/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift b/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift index 0f6042e599..d2ade9cc59 100644 --- a/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift +++ b/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift @@ -16,25 +16,6 @@ import Foundation #endif @testable import RevenueCat -class MockAdClientProxy: AfficheClientProxy { - - static var mockAttributionDetails: [String: NSObject] = [ - "Version3.1": - [ - "iad-campaign-id": 15292426, - "iad-attribution": true - ] as NSObject - ] - static var mockError: Error? - static var requestAttributionDetailsCallCount = 0 - - override func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { - Self.requestAttributionDetailsCallCount += 1 - completionHandler(Self.mockAttributionDetails, Self.mockError) - } - -} - @available(iOS 14, macOS 11, tvOS 14, *) class MockTrackingManagerProxy: TrackingManagerProxy { @@ -48,12 +29,6 @@ class MockTrackingManagerProxy: TrackingManagerProxy { class MockAttributionTypeFactory: AttributionTypeFactory { - static var shouldReturnAdClientProxy = true - - override func afficheClientProxy() -> AfficheClientProxy? { - Self.shouldReturnAdClientProxy ? MockAdClientProxy() : nil - } - static var shouldReturnTrackingManagerProxy = true override func atFollowingProxy() -> TrackingManagerProxy? { diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index cd38ecb74f..7e6e05d98f 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -121,27 +121,6 @@ class MockBackend: Backend { completion(stubbedGetOfferingsCompletionResult!) } - var invokedPostAttributionData = false - var invokedPostAttributionDataCount = 0 - var invokedPostAttributionDataParameters: (data: [String: Any]?, network: AttributionNetwork, appUserID: String?)? - var invokedPostAttributionDataParametersList = [(data: [String: Any]?, - network: AttributionNetwork, - appUserID: String?)]() - var stubbedPostAttributionDataCompletionResult: (BackendError?, Void)? - - override func post(attributionData: [String: Any], - network: AttributionNetwork, - appUserID: String, - completion: ((BackendError?) -> Void)?) { - invokedPostAttributionData = true - invokedPostAttributionDataCount += 1 - invokedPostAttributionDataParameters = (attributionData, network, appUserID) - invokedPostAttributionDataParametersList.append((attributionData, network, appUserID)) - if let result = stubbedPostAttributionDataCompletionResult { - completion?(result.0) - } - } - var invokedCreateAlias = false var invokedCreateAliasCount = 0 var invokedCreateAliasParameters: (appUserID: String?, newAppUserID: String?)? diff --git a/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift deleted file mode 100644 index 14616130f2..0000000000 --- a/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright RevenueCat Inc. All Rights Reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// BackendPostAttributionDataTests.swift -// -// Created by Nacho Soto on 3/7/22. - -import Foundation -import Nimble -import XCTest - -@testable import RevenueCat - -class BackendPostAttributionDataTests: BaseBackendTests { - - override func createClient() -> MockHTTPClient { - super.createClient(#file) - } - - func testPostAttributesPutsDataInDataKey() throws { - self.httpClient.mock( - requestPath: .postAttributionData(appUserID: Self.userID), - response: .init(statusCode: .success) - ) - - let data: [String: AnyObject] = ["a": "b" as NSString, "c": "d" as NSString] - - backend.post(attributionData: data, - network: AttributionNetwork.appleSearchAds, - appUserID: Self.userID, - completion: nil) - - expect(self.httpClient.calls).toEventually(haveCount(1)) - } - -} diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json deleted file mode 100644 index 8372d04da1..0000000000 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "headers" : { - "Authorization" : "Bearer asharedsecret" - }, - "request" : { - "body" : { - "data" : { - "a" : "b", - "c" : "d" - }, - "network" : 0 - }, - "method" : "POST", - "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/user\/attribution" - } -} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json deleted file mode 100644 index 8372d04da1..0000000000 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "headers" : { - "Authorization" : "Bearer asharedsecret" - }, - "request" : { - "body" : { - "data" : { - "a" : "b", - "c" : "d" - }, - "network" : 0 - }, - "method" : "POST", - "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/user\/attribution" - } -} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json deleted file mode 100644 index 8372d04da1..0000000000 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "headers" : { - "Authorization" : "Bearer asharedsecret" - }, - "request" : { - "body" : { - "data" : { - "a" : "b", - "c" : "d" - }, - "network" : 0 - }, - "method" : "POST", - "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/user\/attribution" - } -} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json deleted file mode 100644 index 8372d04da1..0000000000 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "headers" : { - "Authorization" : "Bearer asharedsecret" - }, - "request" : { - "body" : { - "data" : { - "a" : "b", - "c" : "d" - }, - "network" : 0 - }, - "method" : "POST", - "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/user\/attribution" - } -} \ No newline at end of file diff --git a/Tests/UnitTests/Purchasing/PurchasesTests.swift b/Tests/UnitTests/Purchasing/PurchasesTests.swift index 61e409a031..599a8195d7 100644 --- a/Tests/UnitTests/Purchasing/PurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/PurchasesTests.swift @@ -210,32 +210,6 @@ class PurchasesTests: XCTestCase { } } - var invokedPostAttributionData = false - var invokedPostAttributionDataCount = 0 - // swiftlint:disable:next large_tuple - var invokedPostAttributionDataParameters: ( - data: [String: Any]?, - network: AttributionNetwork, - appUserID: String? - )? - var invokedPostAttributionDataParametersList = [(data: [String: Any]?, - network: AttributionNetwork, - appUserID: String?)]() - var stubbedPostAttributionDataCompletionResult: (BackendError?, Void)? - - override func post(attributionData: [String: Any], - network: AttributionNetwork, - appUserID: String, - completion: ((BackendError?) -> Void)? = nil) { - invokedPostAttributionData = true - invokedPostAttributionDataCount += 1 - invokedPostAttributionDataParameters = (attributionData, network, appUserID) - invokedPostAttributionDataParametersList.append((attributionData, network, appUserID)) - if let result = stubbedPostAttributionDataCompletionResult { - completion?(result.0) - } - } - var postOfferForSigningCalled = false var postOfferForSigningPaymentDiscountResponse: Result<[String: Any], BackendError> = .success([:]) @@ -289,14 +263,18 @@ class PurchasesTests: XCTestCase { var purchases: Purchases! func setupPurchases(automaticCollection: Bool = false) { - Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + Purchases.automaticAdServicesAttributionTokenCollection = automaticCollection + } self.identityManager.mockIsAnonymous = false initializePurchasesInstance(appUserId: identityManager.currentAppUserID) } func setupAnonPurchases() { - Purchases.automaticAppleSearchAdsAttributionCollection = false + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + Purchases.automaticAdServicesAttributionTokenCollection = false + } self.identityManager.mockIsAnonymous = true initializePurchasesInstance(appUserId: nil) } @@ -1863,22 +1841,6 @@ class PurchasesTests: XCTestCase { expect(attributionData?["rc_idfv"] as? String) == "rc_idfv" } - func testPassesTheArrayForAllNetworks() { - setupPurchases() - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, fromNetwork: AttributionNetwork.appleSearchAds) - - for key in data.keys { - expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains(key)) - .toEventually(beTrue()) - } - expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains("rc_idfa")) == true - expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains("rc_idfv")) == true - expect(self.backend.invokedPostAttributionDataParametersList[0].network) == AttributionNetwork.appleSearchAds - expect(self.backend.invokedPostAttributionDataParametersList[0].appUserID) == self.purchases?.appUserID - } - func testSharedInstanceIsSetWhenConfiguring() { let purchases = Purchases.configure(withAPIKey: "") expect(Purchases.shared) === purchases @@ -2158,74 +2120,24 @@ class PurchasesTests: XCTestCase { expect(invokedParameters?.appUserID) == self.purchases?.appUserID } - func testAttributionDataSendsNetworkAppUserId() throws { - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, - from: AttributionNetwork.appleSearchAds, - forNetworkUserId: "newuser") - - setupPurchases() - - expect(self.backend.invokedPostAttributionData).toEventually(beTrue()) - - let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) - for key in data.keys { - expect(invokedMethodParams.data?.keys.contains(key)).to(beTrue()) + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + func testAdServicesAttributionTokenIsAutomaticallyCollected() throws { + guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { + throw XCTSkip("Required API is not available for this test.") } - expect(invokedMethodParams.data?.keys.contains("rc_idfa")) == true - expect(invokedMethodParams.data?.keys.contains("rc_idfv")) == true - expect(invokedMethodParams.data?.keys.contains("rc_attribution_network_id")) == true - expect(invokedMethodParams.data?["rc_attribution_network_id"] as? String) == "newuser" - expect(invokedMethodParams.network) == AttributionNetwork.appleSearchAds - expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID + setupPurchases(automaticCollection: true) + expect(self.attributionFetcher.adServicesTokenCollectionCalled) == true } - func testAttributionDataDontSendNetworkAppUserIdIfNotProvided() throws { - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, fromNetwork: AttributionNetwork.appleSearchAds) - - setupPurchases() - - let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) - for key in data.keys { - expect(invokedMethodParams.data?.keys.contains(key)) == true + @available(iOS 14.3, *) + func testAdServicesAttributionTokenIsNotAutomaticallyCollectedIfDisabled() throws { + guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { + throw XCTSkip("Required API is not available for this test.") } - expect(invokedMethodParams.data?.keys.contains("rc_idfa")) == true - expect(invokedMethodParams.data?.keys.contains("rc_idfv")) == true - expect(invokedMethodParams.data?.keys.contains("rc_attribution_network_id")) == false - expect(invokedMethodParams.network) == AttributionNetwork.appleSearchAds - expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID - } - - func testAdClientAttributionDataIsAutomaticallyCollected() throws { - setupPurchases(automaticCollection: true) - - let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) - - expect(invokedMethodParams).toNot(beNil()) - expect(invokedMethodParams.network) == AttributionNetwork.appleSearchAds - - let obtainedVersionData = try XCTUnwrap(invokedMethodParams.data?["Version3.1"] as? NSDictionary) - expect(obtainedVersionData["iad-campaign-id"]).toNot(beNil()) - } - - func testAdClientAttributionDataIsNotAutomaticallyCollectedIfDisabled() { setupPurchases(automaticCollection: false) - expect(self.backend.invokedPostAttributionDataParameters).to(beNil()) - } - - func testAttributionDataPostponesMultiple() { - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, from: AttributionNetwork.adjust, forNetworkUserId: "newuser") - - setupPurchases(automaticCollection: true) - expect(self.backend.invokedPostAttributionDataParametersList.count) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParametersList.count) == 1 + expect(self.attributionFetcher.adServicesTokenCollectionCalled) == false } func testObserverModeSetToFalseSetFinishTransactions() throws { diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index f1845d9812..3770759cdb 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -126,8 +126,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { UserDefaults().removePersistentDomain(forName: "TestDefaults") } - func setupPurchases(automaticCollection: Bool = false) { - Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection + func setupPurchases() { self.mockIdentityManager.mockIsAnonymous = false let purchasesOrchestrator = PurchasesOrchestrator(productsManager: mockProductsManager, storeKitWrapper: mockStoreKitWrapper, From cbe98195090c74f7fe429ff42d5a4d501224da41 Mon Sep 17 00:00:00 2001 From: beylmk Date: Thu, 9 Jun 2022 17:38:37 -0600 Subject: [PATCH 02/16] [CF-553] Post AdServices token (#1534) Co-authored-by: NachoSoto Co-authored-by: Josh Holtz --- RevenueCat.xcodeproj/project.pbxproj | 40 +- Sources/Attribution/AttributionPoster.swift | 43 +- Sources/Caching/DeviceCache.swift | 27 +- .../Logging/Strings/AttributionStrings.swift | 13 + Sources/Networking/Backend.swift | 12 + Sources/Networking/HTTPRequest.swift | 4 + .../PostAdServicesTokenOperation.swift | 69 + Sources/Purchasing/Purchases.swift | 9 + .../SwiftAPITester/PurchasesAPI.swift | 13 +- .../Attribution/AttributionPosterTests.swift | 84 +- .../Mocks/MockAttributionFetcher.swift | 3 +- Tests/UnitTests/Mocks/MockBackend.swift | 26 +- Tests/UnitTests/Mocks/MockDeviceCache.swift | 21 + .../MockSubscriberAttributesManager.swift | 4 +- .../BackendPostAdServicesTokenTests.swift | 42 + ...4-testPostAdServicesCallsHttpClient.1.json | 12 + ...5-testPostAdServicesCallsHttpClient.1.json | 12 + .../UnitTests/Purchasing/PurchasesTests.swift | 2637 ----------------- .../SubscriberAttributesManagerTests.swift | 2 +- 19 files changed, 372 insertions(+), 2701 deletions(-) create mode 100644 Sources/Networking/Operations/PostAdServicesTokenOperation.swift create mode 100644 Tests/UnitTests/Networking/Backend/BackendPostAdServicesTokenTests.swift create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS15-testPostAdServicesCallsHttpClient.1.json delete mode 100644 Tests/UnitTests/Purchasing/PurchasesTests.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index fda052e974..7a7db2701f 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -125,7 +125,6 @@ 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 */; }; 351B514126D4498F00BD2BD7 /* MockBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514026D4498F00BD2BD7 /* MockBackend.swift */; }; @@ -238,20 +237,20 @@ 57554CC1282AE1E3009A7E58 /* TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57554CC0282AE1E3009A7E58 /* TestCase.swift */; }; 57554CC2282AE1E3009A7E58 /* TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57554CC0282AE1E3009A7E58 /* TestCase.swift */; }; 575A17AB2773A59300AA6F22 /* CurrentTestCaseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */; }; - 5766AA56283D4C5400FA6091 /* IgnoreHashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */; }; - 5766AA5A283D4CAB00FA6091 /* IgnoreHashableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA59283D4CAB00FA6091 /* IgnoreHashableTests.swift */; }; - 5766AB4728401B8400FA6091 /* PackageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AB4628401B8400FA6091 /* PackageType.swift */; }; - 5766C620282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */; }; - 5766C622282DAA700067D886 /* GetIntroEligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */; }; 5766AA3E283C750300FA6091 /* Operators+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA3D283C750300FA6091 /* Operators+Extensions.swift */; }; 5766AA42283C768600FA6091 /* OperatorExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA41283C768600FA6091 /* OperatorExtensionsTests.swift */; }; + 5766AA56283D4C5400FA6091 /* IgnoreHashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */; }; + 5766AA5A283D4CAB00FA6091 /* IgnoreHashableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA59283D4CAB00FA6091 /* IgnoreHashableTests.swift */; }; + 5766AAB0283D8CDC00FA6091 /* CacheFetchPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAAF283D8CDC00FA6091 /* CacheFetchPolicy.swift */; }; 5766AABF283E80B500FA6091 /* BasePurchasesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AABE283E80B500FA6091 /* BasePurchasesTests.swift */; }; 5766AAC5283E843300FA6091 /* PurchasesConfiguringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAC4283E843300FA6091 /* PurchasesConfiguringTests.swift */; }; 5766AAC9283E88CF00FA6091 /* PurchasesGetCustomerInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAC8283E88CF00FA6091 /* PurchasesGetCustomerInfoTests.swift */; }; 5766AAD1283E981700FA6091 /* PurchasesPurchasingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAD0283E981700FA6091 /* PurchasesPurchasingTests.swift */; }; 5766AAD5283E9B7400FA6091 /* PurchasesRestoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAD4283E9B7400FA6091 /* PurchasesRestoreTests.swift */; }; 5766AAE5283E9E9C00FA6091 /* PurchasesGetOfferingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAE4283E9E9C00FA6091 /* PurchasesGetOfferingsTests.swift */; }; - 5766AAB0283D8CDC00FA6091 /* CacheFetchPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AAAF283D8CDC00FA6091 /* CacheFetchPolicy.swift */; }; + 5766AB4728401B8400FA6091 /* PackageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AB4628401B8400FA6091 /* PackageType.swift */; }; + 5766C620282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */; }; + 5766C622282DAA700067D886 /* GetIntroEligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */; }; 576C8A8B27CFCB150058FA6E /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A8A27CFCB150058FA6E /* AnyEncodable.swift */; }; 576C8A8F27CFCD110058FA6E /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A8E27CFCD110058FA6E /* AnyEncodableTests.swift */; }; 576C8A9227D27DDD0058FA6E /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; }; @@ -337,12 +336,15 @@ 9A65E0A02591A23200DE00B0 /* OfferingStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E09F2591A23200DE00B0 /* OfferingStrings.swift */; }; 9A65E0A52591A23500DE00B0 /* PurchaseStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E0A42591A23500DE00B0 /* PurchaseStrings.swift */; }; 9A65E0AA2591A23800DE00B0 /* RestoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E0A92591A23800DE00B0 /* RestoreStrings.swift */; }; + A524378B284FFF0200E788BD /* AttributionPosterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A524378A284FFF0200E788BD /* AttributionPosterTests.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 */; }; A55D083627236F0200D919E0 /* MockSK2BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55D08312722471200D919E0 /* MockSK2BeginRefundRequestHelper.swift */; }; + A55D5D66282ECCC100FA7623 /* PostAdServicesTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55D5D65282ECCC100FA7623 /* PostAdServicesTokenOperation.swift */; }; A563F586271E072B00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; A563F589271E1DAD00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; + A56C2E012819C33500995421 /* BackendPostAdServicesTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */; }; A56F9AB126990E9200AFC48F /* CustomerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56F9AB026990E9200AFC48F /* CustomerInfo.swift */; }; A5B6CDD8280F3843007629D5 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5B6CDD5280F3843007629D5 /* AdServices.framework */; platformFilters = (ios, maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; }; A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */; }; @@ -654,7 +656,6 @@ 37E3548189DA008320B3FC98 /* ProductRequestDataInitializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductRequestDataInitializationTests.swift; sourceTree = ""; }; 37E354B13440508B46C9A530 /* MockReceiptParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockReceiptParser.swift; sourceTree = ""; }; 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroEligibilityCalculatorTests.swift; sourceTree = ""; }; - 37E354FE32DD3EA3FF3ECD0A /* AttributionPosterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionPosterTests.swift; sourceTree = ""; }; 37E355744D64075AA91342DE /* MockInAppPurchaseBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockInAppPurchaseBuilder.swift; sourceTree = ""; }; 37E3567189CF6A746EE3CCC2 /* DateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; 37E3567E972B9B04FE079ABA /* SubscriberAttributesManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberAttributesManagerTests.swift; sourceTree = ""; }; @@ -715,20 +716,20 @@ 57554C87282AC293009A7E58 /* PurchaseOwnershipTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseOwnershipTypeTests.swift; sourceTree = ""; }; 57554CC0282AE1E3009A7E58 /* TestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCase.swift; sourceTree = ""; }; 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTestCaseTracker.swift; sourceTree = ""; }; - 5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreHashable.swift; sourceTree = ""; }; - 5766AA59283D4CAB00FA6091 /* IgnoreHashableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreHashableTests.swift; sourceTree = ""; }; - 5766AB4628401B8400FA6091 /* PackageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageType.swift; sourceTree = ""; }; - 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetIntroEligibilityDecodingTests.swift; sourceTree = ""; }; - 5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetIntroEligibilityResponse.swift; sourceTree = ""; }; 5766AA3D283C750300FA6091 /* Operators+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Operators+Extensions.swift"; sourceTree = ""; }; 5766AA41283C768600FA6091 /* OperatorExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorExtensionsTests.swift; sourceTree = ""; }; + 5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreHashable.swift; sourceTree = ""; }; + 5766AA59283D4CAB00FA6091 /* IgnoreHashableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreHashableTests.swift; sourceTree = ""; }; + 5766AAAF283D8CDC00FA6091 /* CacheFetchPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheFetchPolicy.swift; sourceTree = ""; }; 5766AABE283E80B500FA6091 /* BasePurchasesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePurchasesTests.swift; sourceTree = ""; }; 5766AAC4283E843300FA6091 /* PurchasesConfiguringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesConfiguringTests.swift; sourceTree = ""; }; 5766AAC8283E88CF00FA6091 /* PurchasesGetCustomerInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesGetCustomerInfoTests.swift; sourceTree = ""; }; 5766AAD0283E981700FA6091 /* PurchasesPurchasingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesPurchasingTests.swift; sourceTree = ""; }; 5766AAD4283E9B7400FA6091 /* PurchasesRestoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesRestoreTests.swift; sourceTree = ""; }; 5766AAE4283E9E9C00FA6091 /* PurchasesGetOfferingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesGetOfferingsTests.swift; sourceTree = ""; }; - 5766AAAF283D8CDC00FA6091 /* CacheFetchPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheFetchPolicy.swift; sourceTree = ""; }; + 5766AB4628401B8400FA6091 /* PackageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageType.swift; sourceTree = ""; }; + 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetIntroEligibilityDecodingTests.swift; sourceTree = ""; }; + 5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetIntroEligibilityResponse.swift; sourceTree = ""; }; 576C8A8A27CFCB150058FA6E /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; 576C8A8E27CFCD110058FA6E /* AnyEncodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodableTests.swift; sourceTree = ""; }; 576C8A9027D180540058FA6E /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; @@ -806,11 +807,14 @@ 9A65E09F2591A23200DE00B0 /* OfferingStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OfferingStrings.swift; sourceTree = ""; }; 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 = ""; }; + A524378A284FFF0200E788BD /* AttributionPosterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPosterTests.swift; sourceTree = ""; }; A525BF4A26C320D100C354C4 /* SubscriberAttributesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriberAttributesManager.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 = ""; }; + A55D5D65282ECCC100FA7623 /* PostAdServicesTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAdServicesTokenOperation.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 = ""; }; + A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostAdServicesTokenTests.swift; sourceTree = ""; }; A56F9AB026990E9200AFC48F /* CustomerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfo.swift; sourceTree = ""; }; A5B6CDD5280F3843007629D5 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/AdServices.framework; sourceTree = DEVELOPER_DIR; }; A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelper.swift; sourceTree = ""; }; @@ -1688,6 +1692,7 @@ children = ( 5796A39727D6C07D00653165 /* __Snapshots__ */, 5796A38D27D6BB7D00653165 /* BackendCreateAliasTests.swift */, + A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */, 5796A38927D6B96300653165 /* BackendGetCustomerInfoTests.swift */, 5796A38F27D6BCD100653165 /* BackendGetIntroEligibilityTests.swift */, 5796A39327D6BD6900653165 /* BackendGetOfferingsTests.swift */, @@ -1764,6 +1769,7 @@ B34605AD279A6E380031CA74 /* PostOfferForSigningOperation.swift */, B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */, B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */, + A55D5D65282ECCC100FA7623 /* PostAdServicesTokenOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1848,8 +1854,8 @@ F5BE444626985E6E00254A30 /* Attribution */ = { isa = PBXGroup; children = ( - 37E354FE32DD3EA3FF3ECD0A /* AttributionPosterTests.swift */, 37E350420D54B99BB39448E0 /* AttributionTypeFactoryTests.swift */, + A524378A284FFF0200E788BD /* AttributionPosterTests.swift */, ); path = Attribution; sourceTree = ""; @@ -2249,6 +2255,7 @@ 57DE806D28074976008D6C6F /* Storefront.swift in Sources */, B3B5FBB6269CED6400104A0C /* ErrorDetails.swift in Sources */, 2D991ACA268BA56900085481 /* StoreKitRequestFetcher.swift in Sources */, + A55D5D66282ECCC100FA7623 /* PostAdServicesTokenOperation.swift in Sources */, B3B5FBB4269CED4B00104A0C /* BackendErrorCode.swift in Sources */, 2DDF41B624F6F387005BC22D /* ASN1ObjectIdentifierBuilder.swift in Sources */, 35D832D2262E56DB00E60AC5 /* HTTPStatusCode.swift in Sources */, @@ -2469,7 +2476,6 @@ 5796A39027D6BCD100653165 /* BackendGetIntroEligibilityTests.swift in Sources */, 351B514126D4498F00BD2BD7 /* MockBackend.swift in Sources */, B380D69B27726AB500984578 /* DNSCheckerTests.swift in Sources */, - 351B513B26D448F400BD2BD7 /* AttributionPosterTests.swift in Sources */, 5774F9C12805EA3000997128 /* BaseHTTPResponseTest.swift in Sources */, 351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */, 2DDF41CC24F6F4C3005BC22D /* AppleReceiptBuilderTests.swift in Sources */, @@ -2483,8 +2489,10 @@ 576C8A8F27CFCD110058FA6E /* AnyEncodableTests.swift in Sources */, 351B517226D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift in Sources */, 351B51A426D450BC00BD2BD7 /* NSError+RCExtensionsTests.swift in Sources */, + A524378B284FFF0200E788BD /* AttributionPosterTests.swift in Sources */, 57E2230727500BB1002DB06E /* AtomicTests.swift in Sources */, 351B51B826D450E800BD2BD7 /* StoreKitWrapperTests.swift in Sources */, + A56C2E012819C33500995421 /* BackendPostAdServicesTokenTests.swift in Sources */, 5766AAD5283E9B7400FA6091 /* PurchasesRestoreTests.swift in Sources */, FD18ED4E2837F89200C5AA4F /* StoreKitWorkaroundsTests.swift in Sources */, B3CD0D8827F25E3E000793F5 /* BackendSubscriberAttributesTests.swift in Sources */, diff --git a/Sources/Attribution/AttributionPoster.swift b/Sources/Attribution/AttributionPoster.swift index 48cb037783..8556e46416 100644 --- a/Sources/Attribution/AttributionPoster.swift +++ b/Sources/Attribution/AttributionPoster.swift @@ -53,10 +53,9 @@ class AttributionPoster { } let currentAppUserID = self.currentUserProvider.currentAppUserID - let networkKey = String(network.rawValue) let latestNetworkIdsAndAdvertisingIdsSentByNetwork = - deviceCache.latestNetworkAndAdvertisingIdsSent(appUserID: currentAppUserID) - let latestSentToNetwork = latestNetworkIdsAndAdvertisingIdsSentByNetwork[networkKey] + deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: currentAppUserID) + let latestSentToNetwork = latestNetworkIdsAndAdvertisingIdsSentByNetwork[network] let newValueForNetwork = "\(identifierForAdvertisers ?? "(null)")_\(networkUserId ?? "(null)")" guard latestSentToNetwork != newValueForNetwork else { @@ -65,7 +64,8 @@ class AttributionPoster { } var newDictToCache = latestNetworkIdsAndAdvertisingIdsSentByNetwork - newDictToCache[networkKey] = newValueForNetwork + newDictToCache[network] = newValueForNetwork + var newData = data if let identifierForAdvertisers = identifierForAdvertisers { @@ -96,6 +96,8 @@ class AttributionPoster { // should match OS availability in https://developer.apple.com/documentation/ad_services @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) func postAdServicesTokenIfNeeded() { let latestTokenSent = latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) guard latestTokenSent == nil else { @@ -106,8 +108,7 @@ class AttributionPoster { return } - Logger.debug("Logging attribution token for now to avoid lint warning: \(attributionToken)") - // post + self.post(adServicesToken: attributionToken) } func postPostponedAttributionDataIfNeeded() { @@ -134,22 +135,42 @@ class AttributionPoster { postponedAttributionData = postponedData } + private func post(adServicesToken: String) { + let currentAppUserID = self.currentUserProvider.currentAppUserID + + // set the cache in advance to avoid multiple post calls + var newDictToCache = self.deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: currentAppUserID) + newDictToCache[AttributionNetwork.adServices] = adServicesToken + self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: currentAppUserID) + + backend.post(adServicesToken: adServicesToken, appUserID: currentAppUserID) { error in + guard let error = error else { + Logger.debug(Strings.attribution.adservices_token_post_succeeded) + return + } + Logger.warn(Strings.attribution.adservices_token_post_failed(error: error)) + + // if there's an error, reset the cache + newDictToCache[AttributionNetwork.adServices] = nil + self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: currentAppUserID) + } + } + private func latestNetworkIdAndAdvertisingIdentifierSent(network: AttributionNetwork) -> String? { - let networkID = String(network.rawValue) - let cachedDict = deviceCache.latestNetworkAndAdvertisingIdsSent( + let cachedDict = deviceCache.latestAdvertisingIdsByNetworkSent( appUserID: self.currentUserProvider.currentAppUserID ) - return cachedDict[networkID] + return cachedDict[network] } private func postSubscriberAttributes(newData: [String: Any], network: AttributionNetwork, appUserID: String, - newDictToCache: [String: String]) { + newDictToCache: [AttributionNetwork: String]) { subscriberAttributesManager.setAttributes(fromAttributionData: newData, network: network, appUserID: appUserID) - deviceCache.set(latestNetworkAndAdvertisingIdsSent: newDictToCache, appUserID: appUserID) + deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: appUserID) } } diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index 0712f72cd8..f26b1b619d 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -269,17 +269,34 @@ class DeviceCache { // MARK: - attribution - func latestNetworkAndAdvertisingIdsSent(appUserID: String) -> [String: String] { + func latestAdvertisingIdsByNetworkSent(appUserID: String) -> [AttributionNetwork: String] { return self.userDefaults.read { let key = CacheKeyBases.attributionDataDefaults + appUserID - let latestNetworkAndAdvertisingIdsSent = $0.object(forKey: key) as? [String: String] ?? [:] - return latestNetworkAndAdvertisingIdsSent + let latestAdvertisingIdsByRawNetworkSent = $0.object(forKey: key) as? [String: String] ?? [:] + + // convert keys from UserDefault from Integer String to AttributionNetwork + let latestSent: [AttributionNetwork: String] = + latestAdvertisingIdsByRawNetworkSent.reduce(into: [:]) { adIdsByNetwork, adIdByRawNetworkString in + if let networkRawValue = Int(adIdByRawNetworkString.key), + let attributionNetwork = AttributionNetwork(rawValue: networkRawValue) { + adIdsByNetwork[attributionNetwork] = adIdByRawNetworkString.value + } else { + Logger.error(Strings.attribution.latest_attribution_sent_user_defaults_invalid) + } + } + + return latestSent } } - func set(latestNetworkAndAdvertisingIdsSent: [String: String], appUserID: String) { + func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { self.userDefaults.write { - $0.setValue(latestNetworkAndAdvertisingIdsSent, + // convert AttributionNetwork to Integer as String + let latestAdIdsByRawNetworkStringSent = + latestAdvertisingIdsByNetworkSent.reduce(into: [:]) { adIdsByRawNetworkString, adIdByNetwork in + adIdsByRawNetworkString[String(adIdByNetwork.key.rawValue)] = adIdByNetwork.value + } + $0.setValue(latestAdIdsByRawNetworkStringSent, forKey: CacheKeyBases.attributionDataDefaults + appUserID) } } diff --git a/Sources/Logging/Strings/AttributionStrings.swift b/Sources/Logging/Strings/AttributionStrings.swift index b1e511427d..b115745929 100644 --- a/Sources/Logging/Strings/AttributionStrings.swift +++ b/Sources/Logging/Strings/AttributionStrings.swift @@ -40,6 +40,9 @@ enum AttributionStrings { case missing_advertiser_identifiers case adservices_not_supported case adservices_token_fetch_failed(error: Error) + case adservices_token_post_failed(error: BackendError) + case adservices_token_post_succeeded + case latest_attribution_sent_user_defaults_invalid } @@ -122,6 +125,16 @@ extension AttributionStrings: CustomStringConvertible { case .adservices_token_fetch_failed(let error): return "Fetching AdServices attribution token failed with error: \(error.localizedDescription)" + + case .adservices_token_post_failed(let error): + return "Posting AdServices attribution token failed with error: \(error.localizedDescription)" + + case .adservices_token_post_succeeded: + return "AdServices attribution token successfully posted" + + case .latest_attribution_sent_user_defaults_invalid: + return "Attribution data stored in UserDefaults has invalid format." + } } diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index af6f5ff8c7..0b7919a924 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -177,6 +177,18 @@ class Backend { self.operationQueue.addOperation(getIntroEligibilityOperation) } + func post(adServicesToken: String, + appUserID: String, + completion: SimpleResponseHandler?) { + let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.httpClient, + authHeaders: self.authHeaders, + appUserID: appUserID) + let postAdServicesTokenOperation = PostAdServicesTokenOperation(configuration: config, + token: adServicesToken, + responseHandler: completion) + self.operationQueue.addOperation(postAdServicesTokenOperation) + } + } // Testing extension diff --git a/Sources/Networking/HTTPRequest.swift b/Sources/Networking/HTTPRequest.swift index 4ba2835b62..e7aff3f4e8 100644 --- a/Sources/Networking/HTTPRequest.swift +++ b/Sources/Networking/HTTPRequest.swift @@ -69,6 +69,7 @@ extension HTTPRequest { case postOfferForSigning case postReceiptData case postSubscriberAttributes(appUserID: String) + case postAdServicesToken(appUserID: String) } @@ -98,6 +99,9 @@ extension HTTPRequest.Path: CustomStringConvertible { case let .postAttributionData(appUserID): return "subscribers/\(appUserID)/attribution" + case let .postAdServicesToken(appUserID): + return "subscribers/\(appUserID)/adservices_attribution" + case .postOfferForSigning: return "offers" diff --git a/Sources/Networking/Operations/PostAdServicesTokenOperation.swift b/Sources/Networking/Operations/PostAdServicesTokenOperation.swift new file mode 100644 index 0000000000..fc9313d7ab --- /dev/null +++ b/Sources/Networking/Operations/PostAdServicesTokenOperation.swift @@ -0,0 +1,69 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PostAdServicesTokenOperation.swift +// +// Created by Madeline Beyl on 4/20/22. + +import Foundation + +class PostAdServicesTokenOperation: NetworkOperation { + + private let configuration: UserSpecificConfiguration + private let token: String + private let responseHandler: Backend.SimpleResponseHandler? + + init(configuration: UserSpecificConfiguration, + token: String, + responseHandler: Backend.SimpleResponseHandler?) { + self.token = token + self.configuration = configuration + self.responseHandler = responseHandler + + super.init(configuration: configuration) + } + + override func begin(completion: @escaping () -> Void) { + self.post(completion: completion) + } + + private func post(completion: @escaping () -> Void) { + guard let appUserID = try? self.configuration.appUserID.escapedOrError() else { + self.responseHandler?(.missingAppUserID()) + completion() + return + } + + let request = HTTPRequest(method: .post(Body(aadAttributionToken: self.token)), + path: .postAdServicesToken(appUserID: appUserID)) + + self.httpClient.perform( + request, + authHeaders: self.authHeaders + ) { (response: HTTPResponse.Result) in + self.responseHandler?(response.error.map(BackendError.networkError)) + completion() + } + } + +} + +private extension PostAdServicesTokenOperation { + + struct Body: Encodable { + + let aadAttributionToken: String + + init(aadAttributionToken: String) { + self.aadAttributionToken = aadAttributionToken + } + + } + +} diff --git a/Sources/Purchasing/Purchases.swift b/Sources/Purchasing/Purchases.swift index dfa6d9f734..50dc70f483 100644 --- a/Sources/Purchasing/Purchases.swift +++ b/Sources/Purchasing/Purchases.swift @@ -96,6 +96,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void * Should match OS availability in https://developer.apple.com/documentation/ad_services */ @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) @objc public static var automaticAdServicesAttributionTokenCollection: Bool = false /** @@ -382,6 +384,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void trialOrIntroPriceEligibilityChecker: trialOrIntroPriceChecker) } + // swiftlint:disable:next function_body_length init(appUserID: String?, requestFetcher: StoreKitRequestFetcher, receiptFetcher: ReceiptFetcher, @@ -449,10 +452,12 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void subscribeToAppStateNotifications() attributionPoster.postPostponedAttributionDataIfNeeded() +#if os(iOS) || os(macOS) // should match OS availability in https://developer.apple.com/documentation/ad_services if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { postAdServicesTokenIfNeeded() } +#endif self.customerInfoObservationDisposable = customerInfoManager.monitorChanges { [weak self] customerInfo in guard let self = self else { return } @@ -784,6 +789,8 @@ extension Purchases { // should match OS availability in https://developer.apple.com/documentation/ad_services @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) private func postAdServicesTokenIfNeeded() { guard Self.automaticAdServicesAttributionTokenCollection else { return @@ -1899,10 +1906,12 @@ private extension Purchases { updateAllCachesIfNeeded() dispatchSyncSubscriberAttributesIfNeeded() +#if os(iOS) || os(macOS) // should match OS availability in https://developer.apple.com/documentation/ad_services if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { postAdServicesTokenIfNeeded() } +#endif } @objc func applicationWillResignActive(notification: Notification) { diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 6b910b7893..c009e70c5e 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -79,13 +79,6 @@ private func checkStaticMethods() { let canI: Bool = Purchases.canMakePayments() let version = Purchases.frameworkVersion - // both should have deprecation warning - Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: "") - Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: nil) - - // should have deprecation warning 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use - // Purchases.automaticAdServicesAttributionTokenCollection instead - let automaticAppleSearchAdsAttributionCollection: Bool = Purchases.automaticAppleSearchAdsAttributionCollection let logLevel: LogLevel = Purchases.logLevel let proxyUrl: URL? = Purchases.proxyURL let forceUniversalAppStore: Bool = Purchases.forceUniversalAppStore @@ -94,9 +87,8 @@ private func checkStaticMethods() { let isPurchasesConfigured: Bool = Purchases.isConfigured let automaticAdServicesAttributionTokenCollection: Bool = Purchases.automaticAdServicesAttributionTokenCollection - print(canI, version, automaticAppleSearchAdsAttributionCollection, logLevel, proxyUrl!, - forceUniversalAppStore, simulatesAskToBuyInSandbox, sharedPurchases, isPurchasesConfigured, - automaticAdServicesAttributionTokenCollection) + print(canI, version, logLevel, proxyUrl!, forceUniversalAppStore, simulatesAskToBuyInSandbox, + sharedPurchases, isPurchasesConfigured, automaticAdServicesAttributionTokenCollection) } private func checkTypealiases( @@ -271,6 +263,7 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: "") Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: nil) + let _: Bool = Purchases.automaticAppleSearchAdsAttributionCollection purchases.checkTrialOrIntroDiscountEligibility([String]()) { (_: [String: IntroEligibility]) in } diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index 473384d998..10ac0b5b2e 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -7,11 +7,9 @@ // // https://opensource.org/licenses/MIT // -// AttributionFetcherTests.swift -// PurchasesTests -// -// Created by César de la Vega on 7/17/20. +// AttributionPosterTests.swift // +// Created by Madeline Beyl on 6/7/22. import Foundation import Nimble @@ -19,9 +17,9 @@ import XCTest @testable import RevenueCat -class AttributionPosterTests: TestCase { +class BaseAttributionPosterTests: TestCase { - var attributionFetcher: AttributionFetcher! + var attributionFetcher: MockAttributionFetcher! var attributionPoster: AttributionPoster! var deviceCache: MockDeviceCache! var currentUserProvider: MockCurrentUserProvider! @@ -43,7 +41,7 @@ class AttributionPosterTests: TestCase { userDefaults: UserDefaults(suiteName: userDefaultsSuiteName)!) deviceCache.cache(appUserID: userID) backend = MockBackend() - attributionFetcher = AttributionFetcher(attributionFactory: attributionFactory, systemInfo: systemInfo) + attributionFetcher = MockAttributionFetcher(attributionFactory: attributionFactory, systemInfo: systemInfo) subscriberAttributesManager = MockSubscriberAttributesManager( backend: self.backend, deviceCache: self.deviceCache, @@ -57,6 +55,14 @@ class AttributionPosterTests: TestCase { attributionFetcher: attributionFetcher, subscriberAttributesManager: subscriberAttributesManager) resetAttributionStaticProperties() + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + } + + override func tearDown() { + UserDefaults.standard.removePersistentDomain(forName: userDefaultsSuiteName) + UserDefaults.standard.synchronize() + resetAttributionStaticProperties() + super.tearDown() } private func resetAttributionStaticProperties() { @@ -67,51 +73,99 @@ class AttributionPosterTests: TestCase { MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true } - override func tearDown() { - super.tearDown() - UserDefaults.standard.removePersistentDomain(forName: userDefaultsSuiteName) - UserDefaults.standard.synchronize() - resetAttributionStaticProperties() +} + +#if canImport(AdServices) +@available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) +class AdServicesAttributionPosterTests: BaseAttributionPosterTests { + + func testPostAdServicesTokenIfNeededSkipsIfAlreadySent() { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 + + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 1 + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 } + func testPostAdServicesTokenIfNeededSkipsIfNilToken() throws { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + + attributionFetcher.adServicesTokenToReturn = nil + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + + func testPostAdServicesTokenIfNeededDoesNotCacheOnAPIError() throws { + let stubbedError: BackendError = .networkError( + .errorResponse(.init(code: .invalidAPIKey, message: nil), + 400) + ) + + backend.stubbedPostAdServicesTokenCompletionResult = .failure(stubbedError) + + attributionFetcher.adServicesTokenToReturn = nil + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 0 + } + +} +#endif + +class AttributionPosterTests: BaseAttributionPosterTests { + func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" + attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) + + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: userID) + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 - } func testPostAttributionDataDoesntSkipIfNetworkChanged() { let userID = "userID" - + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .facebook, networkUserId: userID) - + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } func testPostAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: "attributionUser1") + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: "attributionUser2") + expect(self.backend.invokedPostAdServicesTokenCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } diff --git a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift index 3276ac4d08..66a0652e07 100644 --- a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift +++ b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift @@ -16,8 +16,9 @@ class MockAttributionFetcher: AttributionFetcher { } var adServicesTokenCollectionCalled = false + var adServicesTokenToReturn: String? = "mockAdServicesToken" override var adServicesToken: String? { adServicesTokenCollectionCalled = true - return nil + return adServicesTokenToReturn } } diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index f03274d80b..53ff9397b5 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -121,11 +121,29 @@ class MockBackend: Backend { completion(stubbedGetOfferingsCompletionResult!) } + var invokedPostAdServicesToken = false + var invokedPostAdServicesTokenCount = 0 + var invokedPostAdServicesTokenParameters: (token: String, appUserID: String?)? + var invokedPostAdServicesTokenParametersList = [(token: String, appUserID: String?)]() + var stubbedPostAdServicesTokenCompletionResult: Result? + + override func post(adServicesToken: String, + appUserID: String, + completion: SimpleResponseHandler?) { + invokedPostAdServicesToken = true + invokedPostAdServicesTokenCount += 1 + invokedPostAdServicesTokenParameters = (adServicesToken, appUserID) + invokedPostAdServicesTokenParametersList.append((adServicesToken, appUserID)) + if let result = stubbedPostAdServicesTokenCompletionResult { + completion?(result.error) + } + } + var invokedCreateAlias = false var invokedCreateAliasCount = 0 var invokedCreateAliasParameters: (appUserID: String?, newAppUserID: String?)? var invokedCreateAliasParametersList = [(appUserID: String?, newAppUserID: String?)]() - var stubbedCreateAliasCompletionResult: (BackendError?, Void)? + var stubbedCreateAliasCompletionResult: Result? override func createAlias(appUserID: String, newAppUserID: String, completion: ((BackendError?) -> Void)?) { invokedCreateAlias = true @@ -133,7 +151,7 @@ class MockBackend: Backend { invokedCreateAliasParameters = (appUserID, newAppUserID) invokedCreateAliasParametersList.append((appUserID, newAppUserID)) if let result = stubbedCreateAliasCompletionResult { - completion?(result.0) + completion?(result.error) } } @@ -176,7 +194,7 @@ class MockBackend: Backend { var invokedPostSubscriberAttributesCount = 0 var invokedPostSubscriberAttributesParameters: (subscriberAttributes: [String: SubscriberAttribute]?, appUserID: String?)? var invokedPostSubscriberAttributesParametersList: [InvokedPostSubscriberAttributesParams] = [] - var stubbedPostSubscriberAttributesCompletionResult: (BackendError?, Void)? + var stubbedPostSubscriberAttributesCompletionResult: Result? override func post(subscriberAttributes: SubscriberAttributeDict, appUserID: String, @@ -188,7 +206,7 @@ class MockBackend: Backend { InvokedPostSubscriberAttributesParams(subscriberAttributes: subscriberAttributes, appUserID: appUserID) ) if let result = stubbedPostSubscriberAttributesCompletionResult { - completion?(result.0) + completion?(result.error) } else { completion?(nil) } diff --git a/Tests/UnitTests/Mocks/MockDeviceCache.swift b/Tests/UnitTests/Mocks/MockDeviceCache.swift index f9e848b281..21f273af96 100644 --- a/Tests/UnitTests/Mocks/MockDeviceCache.swift +++ b/Tests/UnitTests/Mocks/MockDeviceCache.swift @@ -221,4 +221,25 @@ class MockDeviceCache: DeviceCache { invokedClearLatestNetworkAndAdvertisingIdsSentParameters = (appUserID, ()) invokedClearLatestNetworkAndAdvertisingIdsSentParametersList.append((appUserID, ())) } + + var invokedSetLatestNetworkAndAdvertisingIdsSent = false + var invokedSetLatestNetworkAndAdvertisingIdsSentCount = 0 + var invokedSetLatestNetworkAndAdvertisingIdsSentParameters: + (adIdsByNetwork: [AttributionNetwork: String], appUserID: String?)? + var invokedSetLatestNetworkAndAdvertisingIdsSentParametersList = + [(adIdsByNetwork: [AttributionNetwork: String], appUserID: String?)]() + + override func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { + invokedSetLatestNetworkAndAdvertisingIdsSent = true + invokedSetLatestNetworkAndAdvertisingIdsSentCount += 1 + invokedSetLatestNetworkAndAdvertisingIdsSentParameters = (latestAdvertisingIdsByNetworkSent, appUserID) + invokedSetLatestNetworkAndAdvertisingIdsSentParametersList.append( + (latestAdvertisingIdsByNetworkSent, appUserID) + ) + } + + override func latestAdvertisingIdsByNetworkSent(appUserID: String) -> [AttributionNetwork: String] { + return invokedSetLatestNetworkAndAdvertisingIdsSentParameters?.adIdsByNetwork ?? [:] + } + } diff --git a/Tests/UnitTests/Mocks/MockSubscriberAttributesManager.swift b/Tests/UnitTests/Mocks/MockSubscriberAttributesManager.swift index 72bafcb115..fae0b1d913 100644 --- a/Tests/UnitTests/Mocks/MockSubscriberAttributesManager.swift +++ b/Tests/UnitTests/Mocks/MockSubscriberAttributesManager.swift @@ -324,7 +324,9 @@ class MockSubscriberAttributesManager: SubscriberAttributesManager { var invokedConvertAttributionDataAndSetParameters: (attributionData: [String: Any], network: AttributionNetwork, appUserID: String)? var invokedConvertAttributionDataAndSetParametersList = [(attributionData: [String: Any], network: AttributionNetwork, appUserID: String)]() - override func setAttributes(fromAttributionData attributionData: [String: Any], network: AttributionNetwork, appUserID: String) { + override func setAttributes(fromAttributionData attributionData: [String: Any], + network: AttributionNetwork, + appUserID: String) { invokedConvertAttributionDataAndSet = true invokedConvertAttributionDataAndSetCount += 1 invokedConvertAttributionDataAndSetParameters = (attributionData, network, appUserID) diff --git a/Tests/UnitTests/Networking/Backend/BackendPostAdServicesTokenTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostAdServicesTokenTests.swift new file mode 100644 index 0000000000..53d89054a4 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/BackendPostAdServicesTokenTests.swift @@ -0,0 +1,42 @@ +// +// 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 +// +// BackendPostAdServicesTokenTests.swift +// +// Created by Madeline Beyl on 4/27/22. + +import Foundation + +import Nimble +import XCTest + +@testable import RevenueCat + +class BackendPostAdServicesTokenTests: BaseBackendTests { + + override func createClient() -> MockHTTPClient { + super.createClient(#file) + } + + func testPostAdServicesCallsHttpClient() throws { + self.httpClient.mock( + requestPath: .postAdServicesToken(appUserID: Self.userID), + response: .init(statusCode: .success) + ) + + var completionCalled = false + backend.post(adServicesToken: "asdf", + appUserID: "asdf") { _ in + completionCalled = true + } + expect(self.httpClient.calls).toEventually(haveCount(1)) + expect(completionCalled).toEventually(beTrue()) + } + +} diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..7a9dd746e4 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json @@ -0,0 +1,12 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "aad_attribution_token" : "asdf" + }, + "method" : "POST", + "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/asdf\/adservices_attribution" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS15-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS15-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..7a9dd746e4 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS15-testPostAdServicesCallsHttpClient.1.json @@ -0,0 +1,12 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "aad_attribution_token" : "asdf" + }, + "method" : "POST", + "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/asdf\/adservices_attribution" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Purchasing/PurchasesTests.swift b/Tests/UnitTests/Purchasing/PurchasesTests.swift deleted file mode 100644 index 7515243883..0000000000 --- a/Tests/UnitTests/Purchasing/PurchasesTests.swift +++ /dev/null @@ -1,2637 +0,0 @@ -// -// Copyright RevenueCat Inc. All Rights Reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Created by RevenueCat. -// - -import Nimble -import StoreKit -import XCTest - -@testable import RevenueCat - -class PurchasesTests: TestCase { - - let emptyCustomerInfoData: [String: Any] = [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": NSNull() - ]] - - override func setUpWithError() throws { - try super.setUpWithError() - - userDefaults = UserDefaults(suiteName: "TestDefaults") - systemInfo = MockSystemInfo(finishTransactions: true) - deviceCache = MockDeviceCache(systemInfo: self.systemInfo, userDefaults: userDefaults) - requestFetcher = MockRequestFetcher() - mockProductsManager = MockProductsManager(systemInfo: systemInfo) - mockOperationDispatcher = MockOperationDispatcher() - mockReceiptParser = MockReceiptParser() - identityManager = MockIdentityManager(mockAppUserID: "app_user") - mockIntroEligibilityCalculator = MockIntroEligibilityCalculator(productsManager: mockProductsManager, - receiptParser: mockReceiptParser) - let platformInfo = Purchases.PlatformInfo(flavor: "iOS", version: "3.2.1") - let systemInfoAttribution = try MockSystemInfo(platformInfo: platformInfo, - finishTransactions: true) - receiptFetcher = MockReceiptFetcher(requestFetcher: requestFetcher, systemInfo: systemInfoAttribution) - attributionFetcher = MockAttributionFetcher(attributionFactory: MockAttributionTypeFactory(), - systemInfo: systemInfoAttribution) - backend = MockBackend(httpClient: MockHTTPClient(systemInfo: systemInfo, - eTagManager: MockETagManager()), - apiKey: "mockAPIKey", - attributionFetcher: attributionFetcher) - subscriberAttributesManager = - MockSubscriberAttributesManager(backend: self.backend, - deviceCache: self.deviceCache, - operationDispatcher: self.mockOperationDispatcher, - attributionFetcher: self.attributionFetcher, - attributionDataMigrator: AttributionDataMigrator()) - attributionPoster = AttributionPoster(deviceCache: deviceCache, - currentUserProvider: identityManager, - backend: backend, - attributionFetcher: attributionFetcher, - subscriberAttributesManager: subscriberAttributesManager) - customerInfoManager = CustomerInfoManager(operationDispatcher: mockOperationDispatcher, - deviceCache: deviceCache, - backend: backend, - systemInfo: systemInfo) - mockOfferingsManager = MockOfferingsManager(deviceCache: deviceCache, - operationDispatcher: mockOperationDispatcher, - systemInfo: systemInfo, - backend: backend, - offeringsFactory: offeringsFactory, - productsManager: mockProductsManager) - mockManageSubsHelper = MockManageSubscriptionsHelper(systemInfo: systemInfo, - customerInfoManager: customerInfoManager, - currentUserProvider: identityManager) - mockBeginRefundRequestHelper = MockBeginRefundRequestHelper(systemInfo: systemInfo, - customerInfoManager: customerInfoManager, - currentUserProvider: identityManager) - mockTransactionsManager = MockTransactionsManager(storeKit2Setting: systemInfo.storeKit2Setting, - receiptParser: mockReceiptParser) - } - - override func tearDown() { - Purchases.clearSingleton() - deviceCache = nil - purchases = nil - UserDefaults().removePersistentDomain(forName: "TestDefaults") - - super.tearDown() - } - - class MockBackend: Backend { - var userID: String? - var originalApplicationVersion: String? - var originalPurchaseDate: Date? - var timeout = false - var getSubscriberCallCount = 0 - var overrideCustomerInfoResult: Result = .success( - CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]])! - ) - - override func getCustomerInfo(appUserID: String, completion: @escaping Backend.CustomerInfoResponseHandler) { - getSubscriberCallCount += 1 - userID = appUserID - - if !timeout { - let result = self.overrideCustomerInfoResult - DispatchQueue.main.async { - completion(result) - } - } - } - - var postReceiptDataCalled = false - var postedReceiptData: Data? - var postedIsRestore: Bool? - var postedProductID: String? - var postedPrice: Decimal? - var postedPaymentMode: StoreProductDiscount.PaymentMode? - var postedIntroPrice: Decimal? - var postedCurrencyCode: String? - var postedSubscriptionGroup: String? - var postedDiscounts: [StoreProductDiscount]? - var postedOfferingIdentifier: String? - var postedObserverMode: Bool? - - var postReceiptResult: Result? - var aliasError: BackendError? - var aliasCalled = false - - override func post(receiptData: Data, - appUserID: String, - isRestore: Bool, - productData: ProductRequestData?, - presentedOfferingIdentifier: String?, - observerMode: Bool, - subscriberAttributes: [String: SubscriberAttribute]?, - completion: @escaping Backend.CustomerInfoResponseHandler) { - postReceiptDataCalled = true - postedReceiptData = receiptData - postedIsRestore = isRestore - - if let productData = productData { - postedProductID = productData.productIdentifier - postedPrice = productData.price - - postedPaymentMode = productData.paymentMode - postedIntroPrice = productData.introPrice - postedSubscriptionGroup = productData.subscriptionGroup - - postedCurrencyCode = productData.currencyCode - postedDiscounts = productData.discounts - } - - postedOfferingIdentifier = presentedOfferingIdentifier - postedObserverMode = observerMode - completion(postReceiptResult ?? .failure(.missingAppUserID())) - } - - var postedProductIdentifiers: [String]? - - override func getIntroEligibility(appUserID: String, - receiptData: Data, - productIdentifiers: [String], - completion: @escaping IntroEligibilityResponseHandler) { - postedProductIdentifiers = productIdentifiers - - var eligibilities = [String: IntroEligibility]() - for productID in productIdentifiers { - eligibilities[productID] = IntroEligibility(eligibilityStatus: IntroEligibilityStatus.eligible) - } - - completion(eligibilities, nil) - } - - var failOfferings = false - var badOfferingsResponse = false - var gotOfferings = 0 - - override func getOfferings(appUserID: String, completion: @escaping OfferingsResponseHandler) { - gotOfferings += 1 - if failOfferings { - completion(.failure(.unexpectedBackendResponse(.getOfferUnexpectedResponse))) - return - } - if badOfferingsResponse { - completion(.failure(.networkError(.decoding(CodableError.invalidJSONObject(value: [:]), Data())))) - return - } - - completion(.success(.mockResponse)) - } - - override func createAlias(appUserID: String, newAppUserID: String, completion: ((BackendError?) -> Void)?) { - aliasCalled = true - if aliasError != nil { - completion!(aliasError) - } else { - userID = newAppUserID - completion!(nil) - } - } - - var postOfferForSigningCalled = false - var postOfferForSigningPaymentDiscountResponse: Result<[String: Any], BackendError> = .success([:]) - - override func post(offerIdForSigning offerIdentifier: String, - productIdentifier: String, - subscriptionGroup: String?, - receiptData: Data, - appUserID: String, - completion: @escaping OfferSigningResponseHandler) { - postOfferForSigningCalled = true - - completion( - postOfferForSigningPaymentDiscountResponse.map { - ( - // swiftlint:disable:next force_cast line_length - $0["signature"] as! String, $0["keyIdentifier"] as! String, $0["nonce"] as! UUID, $0["timestamp"] as! Int - ) - } - ) - } - } - - var receiptFetcher: MockReceiptFetcher! - var requestFetcher: MockRequestFetcher! - var mockProductsManager: MockProductsManager! - var backend: MockBackend! - let storeKitWrapper = MockStoreKitWrapper() - let notificationCenter = MockNotificationCenter() - var userDefaults: UserDefaults! = nil - let offeringsFactory = MockOfferingsFactory() - var deviceCache: MockDeviceCache! - var subscriberAttributesManager: MockSubscriberAttributesManager! - var identityManager: MockIdentityManager! - var systemInfo: MockSystemInfo! - var mockOperationDispatcher: MockOperationDispatcher! - var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! - var mockReceiptParser: MockReceiptParser! - var mockTransactionsManager: MockTransactionsManager! - var attributionFetcher: MockAttributionFetcher! - var attributionPoster: AttributionPoster! - var customerInfoManager: CustomerInfoManager! - var mockOfferingsManager: MockOfferingsManager! - var purchasesOrchestrator: PurchasesOrchestrator! - var trialOrIntroPriceEligibilityChecker: MockTrialOrIntroPriceEligibilityChecker! - var mockManageSubsHelper: MockManageSubscriptionsHelper! - var mockBeginRefundRequestHelper: MockBeginRefundRequestHelper! - - // swiftlint:disable:next weak_delegate - var purchasesDelegate = MockPurchasesDelegate() - - var purchases: Purchases! - - func setupPurchases(automaticCollection: Bool = false) { - if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - Purchases.automaticAdServicesAttributionTokenCollection = automaticCollection - } - self.identityManager.mockIsAnonymous = false - - initializePurchasesInstance(appUserId: identityManager.currentAppUserID) - } - - func setupAnonPurchases() { - if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - Purchases.automaticAdServicesAttributionTokenCollection = false - } - self.identityManager.mockIsAnonymous = true - initializePurchasesInstance(appUserId: nil) - } - - func setupPurchasesObserverModeOn() throws { - systemInfo = try MockSystemInfo(platformInfo: nil, finishTransactions: false) - initializePurchasesInstance(appUserId: nil) - } - - private func initializePurchasesInstance(appUserId: String?) { - purchasesOrchestrator = PurchasesOrchestrator(productsManager: mockProductsManager, - storeKitWrapper: storeKitWrapper, - systemInfo: systemInfo, - subscriberAttributesManager: subscriberAttributesManager, - operationDispatcher: mockOperationDispatcher, - receiptFetcher: receiptFetcher, - customerInfoManager: customerInfoManager, - backend: backend, - currentUserProvider: identityManager, - transactionsManager: mockTransactionsManager, - deviceCache: deviceCache, - manageSubscriptionsHelper: mockManageSubsHelper, - beginRefundRequestHelper: mockBeginRefundRequestHelper) - trialOrIntroPriceEligibilityChecker = MockTrialOrIntroPriceEligibilityChecker( - systemInfo: systemInfo, - receiptFetcher: receiptFetcher, - introEligibilityCalculator: mockIntroEligibilityCalculator, - backend: backend, - currentUserProvider: identityManager, - operationDispatcher: mockOperationDispatcher, - productsManager: mockProductsManager - ) - purchases = Purchases(appUserID: appUserId, - requestFetcher: requestFetcher, - receiptFetcher: receiptFetcher, - attributionFetcher: attributionFetcher, - attributionPoster: attributionPoster, - backend: backend, - storeKitWrapper: storeKitWrapper, - notificationCenter: notificationCenter, - systemInfo: systemInfo, - offeringsFactory: offeringsFactory, - deviceCache: deviceCache, - identityManager: identityManager, - subscriberAttributesManager: subscriberAttributesManager, - operationDispatcher: mockOperationDispatcher, - customerInfoManager: customerInfoManager, - productsManager: mockProductsManager, - offeringsManager: mockOfferingsManager, - purchasesOrchestrator: purchasesOrchestrator, - trialOrIntroPriceEligibilityChecker: trialOrIntroPriceEligibilityChecker) - - purchasesOrchestrator.delegate = purchases - purchases!.delegate = purchasesDelegate - Purchases.setDefaultInstance(purchases!) - } - - func testIsAbleToBeInitialized() { - setupPurchases() - expect(self.purchases).toNot(beNil()) - } - - func testUsingSharedInstanceWithoutInitializingThrowsAssertion() { - let expectedMessage = "Purchases has not been configured. Please call Purchases.configure()" - expectFatalError(expectedMessage: expectedMessage) { _ = Purchases.shared } - } - - func testUsingSharedInstanceAfterInitializingDoesntThrowAssertion() { - setupPurchases() - expectNoFatalError { _ = Purchases.shared } - } - - func testIsConfiguredReturnsCorrectvalue() { - expect(Purchases.isConfigured) == false - setupPurchases() - expect(Purchases.isConfigured) == true - } - - func testFirstInitializationCallDelegate() { - setupPurchases() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testFirstInitializationFromForegroundDelegateForAnonIfNothingCached() { - systemInfo.stubbedIsApplicationBackgrounded = false - setupPurchases() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testFirstInitializationFromBackgroundDoesntCallDelegateForAnonIfNothingCached() { - systemInfo.stubbedIsApplicationBackgrounded = true - setupPurchases() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(0)) - } - - func testFirstInitializationFromBackgroundCallsDelegateForAnonIfInfoCached() throws { - systemInfo.stubbedIsApplicationBackgrounded = true - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - setupPurchases() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testFirstInitializationFromBackgroundDoesntUpdateCustomerInfoCache() { - systemInfo.stubbedIsApplicationBackgrounded = true - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(0)) - } - - func testFirstInitializationFromForegroundUpdatesCustomerInfoCacheIfNotInUserDefaults() { - systemInfo.stubbedIsApplicationBackgrounded = false - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - } - - func testFirstInitializationFromForegroundUpdatesCustomerInfoCacheIfUserDefaultsCacheStale() { - let staleCacheDateForForeground = Calendar.current.date(byAdding: .minute, value: -20, to: Date())! - self.deviceCache.setCustomerInfoCache(timestamp: staleCacheDateForForeground, - appUserID: identityManager.currentAppUserID) - systemInfo.stubbedIsApplicationBackgrounded = false - - setupPurchases() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - } - - func testFirstInitializationFromForegroundUpdatesCustomerInfoEvenIfCacheValid() { - let staleCacheDateForForeground = Calendar.current.date(byAdding: .minute, value: -2, to: Date())! - self.deviceCache.setCustomerInfoCache(timestamp: staleCacheDateForForeground, - appUserID: identityManager.currentAppUserID) - - systemInfo.stubbedIsApplicationBackgrounded = false - - setupPurchases() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - } - - func testDelegateIsCalledForRandomPurchaseSuccess() throws { - setupPurchases() - - let customerInfo = try CustomerInfo(data: emptyCustomerInfoData) - self.backend.postReceiptResult = .success(customerInfo) - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let customerInfoBeforePurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [:] - ]]) - let customerInfoAfterPurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [product.mockProductIdentifier: []] - ]]) - self.backend.overrideCustomerInfoResult = .success(customerInfoBeforePurchase) - self.backend.postReceiptResult = .success(customerInfoAfterPurchase) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testDelegateIsOnlyCalledOnceIfCustomerInfoTheSame() throws { - setupPurchases() - - let customerInfo1 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0" - ] - ]) - - let customerInfo2 = customerInfo1 - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo1) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo2) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testDelegateIsCalledTwiceIfCustomerInfoTheDifferent() throws { - setupPurchases() - - let customerInfo1 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0" - ] - ]) - - let customerInfo2 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "2.0" - ] - ]) - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo1) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo2) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(3)) - } - - func testDelegateIsNotCalledIfBlockPassed() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedIsRestore).to(beFalse()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testIsAbleToFetchProducts() { - setupPurchases() - var products: [StoreProduct]? - let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.getProducts(productIdentifiers) { (newProducts) in - products = newProducts - } - - expect(products).toEventuallyNot(beNil()) - expect(products).toEventually(haveCount(productIdentifiers.count)) - } - - func testSetsSelfAsStoreKitWrapperDelegate() { - setupPurchases() - expect(self.storeKitWrapper.delegate).to(be(purchasesOrchestrator)) - } - - func testAddsPaymentToWrapper() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - expect(self.storeKitWrapper.payment).toNot(beNil()) - expect(self.storeKitWrapper.payment?.productIdentifier).to(equal(product.productIdentifier)) - } - - func testPurchaseProductCachesProduct() { - setupPurchases() - let sk1Product = MockSK1Product(mockProductIdentifier: "com.product.id1") - let product = StoreProduct(sk1Product: sk1Product) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - expect(self.mockProductsManager.invokedCacheProduct) == true - expect(self.mockProductsManager.invokedCacheProductParameter) == sk1Product - } - - func testDoesntFetchProductDataIfEmptyList() { - setupPurchases() - var completionCalled = false - mockProductsManager.resetMock() - self.purchases.getProducts([]) { _ in - completionCalled = true - } - expect(completionCalled).toEventually(beTrue()) - expect(self.mockProductsManager.invokedProducts) == false - } - - func testTransitioningToPurchasing() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchasing - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beFalse()) - } - - func testTransitioningToPurchasedSendsToBackend() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedIsRestore).to(beFalse()) - } - - func testReceiptsSendsAsRestoreWhenAnon() { - setupAnonPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedIsRestore).to(beTrue()) - } - - func testReceiptsSendsAsNotRestoreWhenAnonymousAndNotAllowingSharingAppStoreAccount() { - setupAnonPurchases() - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = false - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedIsRestore).to(beFalse()) - } - - func testReceiptsSendsAsRestoreWhenNotAnonymousAndAllowingSharingAppStoreAccount() { - setupPurchases() - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = true - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedIsRestore).to(beTrue()) - } - - func testFinishesTransactionsIfSentToBackendCorrectly() throws { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - - func testDoesntFinishTransactionsIfFinishingDisabled() throws { - setupPurchases() - self.purchases?.finishTransactions = false - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beFalse()) - } - - func testSendsProductDataIfProductIsCached() throws { - setupPurchases() - let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.getProducts(productIdentifiers) { (newProducts) in - let product = newProducts[0] - self.purchases.purchase(product: newProducts[0]) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(CustomerInfo(testData: self.emptyCustomerInfoData)!) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedReceiptData).toNot(beNil()) - - expect(self.backend.postedProductID).to(equal(product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(product.price as Decimal)) - - if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { - expect(self.backend.postedPaymentMode).to(equal(StoreProductDiscount.PaymentMode.payAsYouGo)) - expect(self.backend.postedIntroPrice).to(equal(product.introductoryDiscount?.price)) - } else { - expect(self.backend.postedPaymentMode).to(beNil()) - expect(self.backend.postedIntroPrice).to(beNil()) - } - - if #available(iOS 12.0, tvOS 12.0, macOS 10.14, *) { - expect(self.backend.postedSubscriptionGroup).to(equal(product.subscriptionGroupIdentifier)) - } - - if #available(iOS 12.2, *) { - expect(self.backend.postedDiscounts?.count).to(equal(1)) - let postedDiscount: StoreProductDiscount = self.backend.postedDiscounts![0] - expect(postedDiscount.offerIdentifier).to(equal("discount_id")) - expect(postedDiscount.price).to(equal(1.99)) - let expectedPaymentMode = StoreProductDiscount.PaymentMode.payAsYouGo.rawValue - expect(postedDiscount.paymentMode.rawValue).to(equal(expectedPaymentMode)) - } - - expect(self.backend.postedCurrencyCode) == product.priceFormatter!.currencyCode - - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - } - - func testFetchesProductDataIfNotCached() throws { - systemInfo.stubbedIsApplicationBackgrounded = true - setupPurchases() - let sk1Product = MockSK1Product(mockProductIdentifier: "com.product.id1") - let product = StoreProduct(sk1Product: sk1Product) - - let transaction = MockTransaction() - storeKitWrapper.payment = SKPayment(product: sk1Product) - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchasing - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.mockProductsManager.invokedProductsParameters).toEventually(contain([product.productIdentifier])) - - expect(self.backend.postedProductID).toNot(beNil()) - expect(self.backend.postedPrice).toNot(beNil()) - expect(self.backend.postedCurrencyCode).toNot(beNil()) - if #available(iOS 12.2, macOS 10.14.4, *) { - expect(self.backend.postedIntroPrice).toNot(beNil()) - } - } - - func testAfterSendingDoesntFinishTransactionIfBackendError() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - self.backend.postReceiptResult = .failure( - .networkError(.errorResponse( - .init(code: .unknownBackendError, message: nil), - .internalServerError - )) - ) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).to(beFalse()) - } - - func testAfterSendingFinishesFromBackendErrorIfAppropriate() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - self.backend.postReceiptResult = .failure( - .networkError(.errorResponse( - .init(code: .unknownBackendError, message: nil), - .invalidRequest - )) - ) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - - func testNotifiesIfTransactionFailsFromBackend() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - self.backend.postReceiptResult = .failure( - .networkError(.errorResponse( - .init(code: .unknownBackendError, message: nil), - .internalServerError - )) - ) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).to(beFalse()) - } - - func testNotifiesIfTransactionFailsFromStoreKit() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedError: Error? - self.purchases.purchase(product: product) { (_, _, error, _) in - receivedError = error - } - - let transaction = MockTransaction() - transaction.mockError = NSError.init(domain: SKErrorDomain, code: 2, userInfo: nil) - transaction.mockPayment = self.storeKitWrapper.payment! - - self.backend.postReceiptResult = .failure(.missingTransactionProductIdentifier()) - - transaction.mockState = SKPaymentTransactionState.failed - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beFalse()) - expect(self.storeKitWrapper.finishCalled).to(beTrue()) - expect(receivedError).toEventuallyNot(beNil()) - } - - func testCallsDelegateAfterBackendResponse() throws { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - - var customerInfo: CustomerInfo? - var receivedError: Error? - var receivedUserCancelled: Bool? - - let customerInfoBeforePurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [:] - ]]) - let customerInfoAfterPurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [product.productIdentifier: []] - ]]) - self.backend.overrideCustomerInfoResult = .success(customerInfoBeforePurchase) - self.backend.postReceiptResult = .success(customerInfoAfterPurchase) - - self.purchases.purchase(product: product) { (_, info, error, userCancelled) in - customerInfo = info - receivedError = error - receivedUserCancelled = userCancelled - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(customerInfo).toEventually(equal(customerInfoAfterPurchase)) - expect(receivedError).toEventually(beNil()) - expect(self.purchasesDelegate.customerInfoReceivedCount).to(equal(2)) - expect(receivedUserCancelled).toEventually(beFalse()) - } - - func testCompletionBlockOnlyCalledOnce() throws { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - - var callCount = 0 - - self.purchases.purchase(product: product) { (_, _, _, _) in - callCount += 1 - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - self.backend.postReceiptResult = .success(try CustomerInfo(data: emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(callCount).toEventually(equal(1)) - } - - func testCompletionBlockNotCalledForDifferentProducts() throws { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - let otherProduct = MockSK1Product(mockProductIdentifier: "com.product.id2") - - var callCount = 0 - - self.purchases.purchase(product: product) { (_, _, _, _) in - callCount += 1 - } - - let transaction = MockTransaction() - transaction.mockPayment = SKPayment.init(product: otherProduct) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(callCount).toEventually(equal(0)) - } - - func testCallingPurchaseWhileSameProductPendingIssuesError() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - - // First one "works" - self.purchases.purchase(product: product) { (_, _, _, _) in - } - - var receivedInfo: CustomerInfo? - var receivedError: NSError? - var receivedUserCancelled: Bool? - - // Second one issues an error - self.purchases.purchase(product: product) { (_, info, error, userCancelled) in - receivedInfo = info - receivedError = error as NSError? - receivedUserCancelled = userCancelled - } - - expect(receivedInfo).toEventually(beNil()) - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError?.domain).toEventually(equal(RCPurchasesErrorCodeDomain)) - expect(receivedError?.code).toEventually(equal(ErrorCode.operationAlreadyInProgressForProductError.rawValue)) - expect(self.storeKitWrapper.addPaymentCallCount).to(equal(1)) - expect(receivedUserCancelled).toEventually(beFalse()) - } - - func testDoesntIgnorePurchasesThatDoNotHaveApplicationUserNames() { - setupPurchases() - let transaction = MockTransaction() - - let payment = SKMutablePayment() - payment.productIdentifier = "test" - - expect(payment.applicationUsername).to(beNil()) - - transaction.mockPayment = payment - transaction.mockState = SKPaymentTransactionState.purchased - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testDoesntSetWrapperDelegateToNilIfDelegateNil() { - setupPurchases() - purchases!.delegate = nil - - expect(self.storeKitWrapper.delegate).toNot(beNil()) - - purchases!.delegate = purchasesDelegate - - expect(self.storeKitWrapper.delegate).toNot(beNil()) - } - - func testSubscribesToUIApplicationDidBecomeActive() { - setupPurchases() - expect(self.notificationCenter.observers.count).to(equal(2)) - if self.notificationCenter.observers.count > 0 { - let (_, _, name, _) = self.notificationCenter.observers[0] - expect(name).to(equal(SystemInfo.applicationDidBecomeActiveNotification)) - } - } - - func testTriggersCallToBackend() { - setupPurchases() - notificationCenter.fireNotifications() - expect(self.backend.userID).toEventuallyNot(beNil()) - } - - func testAutomaticallyFetchesCustomerInfoOnDidBecomeActiveIfCacheStale() { - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - - self.deviceCache.stubbedIsCustomerInfoCacheStale = true - notificationCenter.fireNotifications() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(2)) - } - - func testDoesntAutomaticallyFetchCustomerInfoOnDidBecomeActiveIfCacheValid() { - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - self.deviceCache.stubbedIsCustomerInfoCacheStale = false - - notificationCenter.fireNotifications() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - } - - func testAutomaticallyCallsDelegateOnDidBecomeActiveAndUpdate() { - setupPurchases() - notificationCenter.fireNotifications() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testDoesntRemoveObservationWhenDelegateNil() { - setupPurchases() - purchases!.delegate = nil - - expect(self.notificationCenter.observers.count).to(equal(2)) - } - - func testRestoringPurchasesPostsTheReceipt() { - setupPurchases() - purchases!.restorePurchases() - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testRestoringPurchasesDoesntPostIfReceiptEmptyAndCustomerInfoLoaded() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "original_app_user_id": "app_user_id", - "first_seen": "2019-07-17T00:05:54Z", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases!.restorePurchases() - - expect(self.backend.postReceiptDataCalled) == false - } - - func testRestoringPurchasesPostsIfReceiptEmptyAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases!.restorePurchases() - - expect(self.backend.postReceiptDataCalled) == true - } - - func testRestoringPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoLoaded() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases!.restorePurchases() - - expect(self.backend.postReceiptDataCalled) == true - } - - func testRestoringPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases!.restorePurchases() - - expect(self.backend.postReceiptDataCalled) == true - } - - func testRestoringPurchasesAlwaysRefreshesAndPostsTheReceipt() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = true - purchases!.restorePurchases() - - expect(self.receiptFetcher.receiptDataTimesCalled).to(equal(1)) - } - - func testRestoringPurchasesSetsIsRestore() { - setupPurchases() - purchases!.restorePurchases() - expect(self.backend.postedIsRestore!).to(beTrue()) - } - - func testRestoringPurchasesSetsIsRestoreForAnon() { - setupAnonPurchases() - purchases!.restorePurchases() - - expect(self.backend.postedIsRestore!).to(beTrue()) - } - - func testRestoringPurchasesCallsSuccessDelegateMethod() throws { - setupPurchases() - - let customerInfo = try CustomerInfo(data: self.emptyCustomerInfoData) - self.backend.postReceiptResult = .success(customerInfo) - - var receivedCustomerInfo: CustomerInfo? - - purchases!.restorePurchases { (info, _) in - receivedCustomerInfo = info - } - - expect(receivedCustomerInfo).toEventually(be(customerInfo)) - } - - func testRestorePurchasesPassesErrorOnFailure() { - setupPurchases() - - let error: BackendError = .missingAppUserID() - - self.backend.postReceiptResult = .failure(error) - self.purchasesDelegate.customerInfo = nil - - var receivedError: Error? - - purchases!.restorePurchases { (_, newError) in - receivedError = newError - } - - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError).to(matchError(error.asPurchasesError)) - } - - func testSyncPurchasesPostsTheReceipt() { - setupPurchases() - purchases.syncPurchases(completion: nil) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testSyncPurchasesPostsTheReceiptIfAutoSyncPurchasesSettingIsOff() throws { - systemInfo = try MockSystemInfo(platformInfo: nil, - finishTransactions: false, - dangerousSettings: DangerousSettings(autoSyncPurchases: false)) - initializePurchasesInstance(appUserId: nil) - - purchases.syncPurchases(completion: nil) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testSyncPurchasesDoesntPostIfReceiptEmptyAndCustomerInfoLoaded() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == false - } - - func testSyncPurchasesPostsIfReceiptEmptyAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoLoaded() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesDoesntRefreshTheReceiptIfNotEmpty() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = true - purchases.syncPurchases(completion: nil) - - expect(self.receiptFetcher.receiptDataTimesCalled) == 1 - expect(self.requestFetcher.refreshReceiptCalled) == false - } - - func testSyncPurchasesDoesntRefreshTheReceiptIfEmpty() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = false - purchases.syncPurchases(completion: nil) - - expect(self.receiptFetcher.receiptDataTimesCalled) == 1 - expect(self.requestFetcher.refreshReceiptCalled) == false - } - - func testSyncPurchasesPassesIsRestoreAsAllowSharingAppStoreAccount() { - setupPurchases() - - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = false - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == false - - deprecated.allowSharingAppStoreAccount = true - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == true - } - - func testSyncPurchasesSetsIsRestoreForAnon() { - setupAnonPurchases() - - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = false - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == false - - deprecated.allowSharingAppStoreAccount = true - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == true - } - - func testSyncPurchasesCallsSuccessDelegateMethod() throws { - setupPurchases() - - let customerInfo = try CustomerInfo(data: self.emptyCustomerInfoData) - self.backend.postReceiptResult = .success(customerInfo) - - var receivedCustomerInfo: CustomerInfo? - - purchases!.syncPurchases { (info, _) in - receivedCustomerInfo = info - } - - expect(receivedCustomerInfo).toEventually(be(customerInfo)) - } - - func testSyncPurchasesPassesErrorOnFailure() { - setupPurchases() - - let error: BackendError = .missingAppUserID() - - self.backend.postReceiptResult = .failure(error) - self.purchasesDelegate.customerInfo = nil - - var receivedError: Error? - - purchases!.syncPurchases { (_, newError) in - receivedError = newError - } - - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError).to(matchError(error.asPurchasesError)) - } - - func testCallsShouldAddPromoPaymentDelegateMethod() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "mock_product")) - let payment = SKPayment() - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product.sk1Product!) - - expect(self.purchasesDelegate.promoProduct) == product - } - - func testShouldAddPromoPaymentDelegateMethodReturnsFalse() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment() - - let result = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(result).to(beFalse()) - } - - func testPromoPaymentDelegateMethodMakesRightCalls() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - let transaction = MockTransaction() - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedProductID).to(equal(product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(product.price as Decimal)) - } - - func testPromoPaymentDelegateMethodCachesProduct() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - let transaction = MockTransaction() - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.mockProductsManager.invokedCacheProduct) == true - expect(self.mockProductsManager.invokedCacheProductParameter) == product - } - - func testDeferBlockMakesPayment() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - guard let storeKitWrapperDelegate = storeKitWrapper.delegate else { - fail("storeKitWrapperDelegate nil") - return - } - - _ = storeKitWrapperDelegate.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(self.purchasesDelegate.makeDeferredPurchase).toNot(beNil()) - - expect(self.storeKitWrapper.payment).to(beNil()) - - guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { - fail("makeDeferredPurchase should have been nonNil") - return - } - - makeDeferredPurchase { (_, _, _, _) in - } - - expect(self.storeKitWrapper.payment).to(be(payment)) - } - - func testGetEligibility() { - setupPurchases() - purchases.checkTrialOrIntroDiscountEligibility(productIdentifiers: []) { (_) in - } - - expect(self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore) - .to(beTrue()) - } - - func testFetchVersionSendsAReceiptIfNoVersion() throws { - setupPurchases() - - self.backend.postReceiptResult = .success(try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ] - ])) - - var receivedCustomerInfo: CustomerInfo? - - purchases?.restorePurchases { (info, _) in - receivedCustomerInfo = info - } - - expect(receivedCustomerInfo?.originalApplicationVersion).toEventually(equal("1.0")) - expect(receivedCustomerInfo?.originalPurchaseDate) - .toEventually(equal(Date(timeIntervalSinceReferenceDate: 562288673))) - expect(self.backend.userID).toEventuallyNot(beNil()) - expect(self.backend.postReceiptDataCalled).toEventuallyNot(beFalse()) - } - - func testCachesCustomerInfo() { - setupPurchases() - - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(1)) - expect(self.deviceCache.cachedCustomerInfo[self.purchases!.appUserID]).toEventuallyNot(beNil()) - - let customerInfo = self.deviceCache.cachedCustomerInfo[self.purchases!.appUserID] - - do { - if customerInfo != nil { - try JSONSerialization.jsonObject(with: customerInfo!, options: []) - } - } catch { - fail() - } - } - - func testCachesCustomerInfoOnPurchase() throws { - setupPurchases() - - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(1)) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]])) - - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - - expect(self.deviceCache.cacheCustomerInfoCount).toEventually(equal(2)) - } - - func testCachedCustomerInfoHasSchemaVersion() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - let jsonObject = info!.jsonObject() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.backend.timeout = true - - setupPurchases() - - var receivedInfo: CustomerInfo? - - purchases!.getCustomerInfo { (info, _) in - receivedInfo = info - } - - expect(receivedInfo).toNot(beNil()) - expect(receivedInfo?.schemaVersion).toNot(beNil()) - } - - func testCachedCustomerInfoHandlesNullSchema() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - - var jsonObject = info!.jsonObject() - - jsonObject["schema_version"] = NSNull() - - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.backend.timeout = true - - setupPurchases() - - var receivedInfo: CustomerInfo? - - purchases!.getCustomerInfo { (info, _) in - receivedInfo = info - } - - expect(receivedInfo).to(beNil()) - } - - func testSendsCachedCustomerInfoToGetter() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - let object = try JSONSerialization.data(withJSONObject: info!.jsonObject(), options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.backend.timeout = true - - setupPurchases() - - var receivedInfo: CustomerInfo? - - purchases!.getCustomerInfo { (info, _) in - receivedInfo = info - } - - expect(receivedInfo).toNot(beNil()) - } - - func testCustomerInfoCompletionBlockCalledExactlyOnceWhenInfoCached() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - let object = try JSONSerialization.data(withJSONObject: info!.jsonObject(), options: []) - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.deviceCache.stubbedIsCustomerInfoCacheStale = true - self.backend.timeout = false - - setupPurchases() - - var callCount = 0 - - purchases!.getCustomerInfo { (_, _) in - callCount += 1 - } - - expect(callCount).toEventually(equal(1)) - } - - func testDoesntSendsCachedCustomerInfoToGetterIfSchemaVersionDiffers() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - var jsonObject = info!.jsonObject() - jsonObject["schema_version"] = "bad_version" - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.backend.timeout = true - - setupPurchases() - - var receivedInfo: CustomerInfo? - - purchases!.getCustomerInfo { (info, _) in - receivedInfo = info - } - - expect(receivedInfo).to(beNil()) - } - - func testDoesntSendsCachedCustomerInfoToGetterIfNoSchemaVersionInCached() throws { - let info = CustomerInfo(testData: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - var jsonObject = info!.jsonObject() - jsonObject.removeValue(forKey: "schema_version") - let object = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - self.backend.timeout = true - - setupPurchases() - - var receivedInfo: CustomerInfo? - - purchases!.getCustomerInfo { (info, _) in - receivedInfo = info - } - - expect(receivedInfo).to(beNil()) - } - - func testDoesntSendCacheIfNoCacheAndCallsBackendAgain() { - self.backend.timeout = true - - setupPurchases() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - - purchases!.getCustomerInfo { (_, _) in - } - - expect(self.backend.getSubscriberCallCount).to(equal(2)) - } - - func testFirstInitializationGetsOfferingsIfAppActive() { - systemInfo.stubbedIsApplicationBackgrounded = false - setupPurchases() - expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(1)) - } - - func testFirstInitializationDoesntFetchOfferingsIfAppBackgrounded() { - systemInfo.stubbedIsApplicationBackgrounded = true - setupPurchases() - expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(0)) - } - - func testProductDataIsCachedForOfferings() throws { - setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = .success( - try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) - ) - self.purchases?.getOfferings { (newOfferings, _) in - let storeProduct = newOfferings!["base"]!.monthly!.storeProduct - let product = storeProduct.sk1Product! - self.purchases.purchase(product: storeProduct) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(CustomerInfo(testData: self.emptyCustomerInfoData)!) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedReceiptData).toNot(beNil()) - - expect(self.backend.postedProductID).to(equal(product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(product.price as Decimal)) - expect(self.backend.postedCurrencyCode).to(equal(product.priceLocale.currencyCode)) - - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - } - - func testAddAttributionAlwaysAddsAdIdsEmptyDict() { - setupPurchases() - - Purchases.deprecated.addAttributionData([:], fromNetwork: AttributionNetwork.adjust) - - // swiftlint:disable:next line_length - let attributionData = self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParameters?.attributionData - expect(attributionData?.count) == 2 - expect(attributionData?["rc_idfa"] as? String) == "rc_idfa" - expect(attributionData?["rc_idfv"] as? String) == "rc_idfv" - } - - func testSharedInstanceIsSetWhenConfiguring() { - let purchases = Purchases.configure(withAPIKey: "") - expect(Purchases.shared) === purchases - } - - func testSharedInstanceIsSetWhenConfiguringWithAppUserID() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "") - expect(Purchases.shared) === purchases - } - - func testSharedInstanceIsSetWhenConfiguringWithObserverMode() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "", observerMode: true) - expect(Purchases.shared) === purchases - expect(Purchases.shared.finishTransactions) == false - } - - func testSharedInstanceIsSetWhenConfiguringWithAppUserIDAndUserDefaults() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "", observerMode: false, userDefaults: nil) - expect(Purchases.shared) === purchases - expect(Purchases.shared.finishTransactions) == true - } - - func testSharedInstanceIsSetWhenConfiguringWithAppUserIDAndUserDefaultsAndUseSK2() { - let purchases = Purchases.configure(withAPIKey: "", - appUserID: "", - observerMode: false, - userDefaults: nil, - useStoreKit2IfAvailable: true) - expect(Purchases.shared) === purchases - expect(Purchases.shared.finishTransactions) == true - } - - func testWhenNoReceiptDataReceiptIsRefreshed() { - setupPurchases() - receiptFetcher.shouldReturnReceipt = true - receiptFetcher.shouldReturnZeroBytesReceipt = true - - makeAPurchase() - - expect(self.receiptFetcher.receiptDataCalled) == true - expect(self.receiptFetcher.receiptDataReceivedRefreshPolicy) == .onlyIfEmpty - } - - private func makeAPurchase() { - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - - guard let purchases = purchases else { fatalError("purchases is not initialized") } - purchases.purchase(product: product) { _, _, _, _ in } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchased - - storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - } - - func testRestoresDontPostMissingReceipts() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = false - var receivedError: NSError? - self.purchases?.restorePurchases { (_, error) in - receivedError = error as NSError? - } - - expect(receivedError?.code).toEventually(equal(ErrorCode.missingReceiptFileError.rawValue)) - } - - func testRestorePurchasesCallsCompletionOnMainThreadWhenMissingReceipts() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = false - var receivedError: NSError? - self.purchases?.restorePurchases { (_, error) in - receivedError = error as NSError? - } - - expect(self.mockOperationDispatcher.invokedDispatchOnMainThreadCount) == 1 - expect(receivedError?.code).toEventually(equal(ErrorCode.missingReceiptFileError.rawValue)) - } - - func testUserCancelledFalseIfPurchaseSuccessful() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - - self.purchases.purchase(product: product) { (_, _, _, userCancelled) in - receivedUserCancelled = userCancelled - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventually(beFalse()) - } - - func testUnknownErrorCurrentlySubscribedIsParsedCorrectly() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - var receivedError: NSError? - var receivedUnderlyingError: NSError? - - let unknownError = NSError( - domain: SKErrorDomain, - code: SKError.unknown.rawValue, - userInfo: [ - NSUnderlyingErrorKey: NSError( - domain: "ASDServerErrorDomain", - code: 3532, - userInfo: [:] - ) - ] - ) - - self.purchases.purchase(product: product) { (_, _, error, userCancelled) in - receivedError = error as NSError? - receivedUserCancelled = userCancelled - // swiftlint:disable:next force_cast - receivedUnderlyingError = receivedError?.userInfo[NSUnderlyingErrorKey] as! NSError? - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = .failed - transaction.mockError = unknownError - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventually(beFalse()) - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError?.domain).toEventually(equal(RCPurchasesErrorCodeDomain)) - expect(receivedError?.code).toEventually(equal(ErrorCode.productAlreadyPurchasedError.rawValue)) - expect(receivedUnderlyingError?.domain).toEventually(equal(unknownError.domain)) - expect(receivedUnderlyingError?.code).toEventually(equal(unknownError.code)) - } - - func testPaymentSheetCancelledErrorIsParsedCorrectly() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - var receivedError: NSError? - - let unknownError = NSError( - domain: SKErrorDomain, - code: 907, - userInfo: [ - NSUnderlyingErrorKey: NSError( - domain: "AMSErrorDomain", - code: 6, - userInfo: [:] - ) - ] - ) - - self.purchases.purchase(product: product) { (_, _, error, userCancelled) in - receivedError = error as NSError? - receivedUserCancelled = userCancelled - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = .failed - transaction.mockError = unknownError - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventuallyNot(beNil()) - expect(receivedUserCancelled) == true - expect(receivedError).to(matchError(ErrorCode.purchaseCancelledError)) - } - - func testUserCancelledTrueIfPurchaseCancelled() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - var receivedError: NSError? - var receivedUnderlyingError: NSError? - - self.purchases.purchase(product: product) { (_, _, error, userCancelled) in - receivedError = error as NSError? - receivedUserCancelled = userCancelled - // swiftlint:disable:next force_cast - receivedUnderlyingError = receivedError?.userInfo[NSUnderlyingErrorKey] as! NSError? - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.failed - transaction.mockError = NSError.init(domain: SKErrorDomain, code: SKError.Code.paymentCancelled.rawValue) - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventuallyNot(beNil()) - - expect(receivedUserCancelled) == true - expect(receivedError).toNot(beNil()) - expect(receivedError?.domain) == RCPurchasesErrorCodeDomain - expect(receivedError?.code) == ErrorCode.purchaseCancelledError.rawValue - expect(receivedUnderlyingError?.domain) == SKErrorDomain - expect(receivedUnderlyingError?.code) == SKError.Code.paymentCancelled.rawValue - } - - func testDoNotSendEmptyReceiptWhenMakingPurchase() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = false - - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - var receivedError: NSError? - - self.purchases.purchase(product: product) { (_, _, error, userCancelled) in - receivedError = error as NSError? - receivedUserCancelled = userCancelled - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventually(beFalse()) - expect(receivedError).toEventuallyNot(beNil()) - expect(self.backend.postReceiptDataCalled).toEventually(beFalse()) - - expect(receivedError?.domain) == RCPurchasesErrorCodeDomain - expect(receivedError?.code) == ErrorCode.missingReceiptFileError.rawValue - } - - func testDeferBlockCallsCompletionBlockAfterPurchaseCompletes() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(self.purchasesDelegate.makeDeferredPurchase).toNot(beNil()) - - expect(self.storeKitWrapper.payment).to(beNil()) - - var completionCalled = false - - guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { - fail("makeDeferredPurchase nil") - return - } - - makeDeferredPurchase { (_, _, _, _) in - completionCalled = true - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.storeKitWrapper.payment).to(be(payment)) - expect(completionCalled).toEventually(beTrue()) - } - - func testAttributionDataIsPostponedIfThereIsNoInstance() { - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, fromNetwork: AttributionNetwork.appsFlyer) - - setupPurchases() - - let invokedParameters = self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParameters - expect(invokedParameters?.attributionData).toNot(beNil()) - - for key in data.keys { - expect(invokedParameters?.attributionData.keys.contains(key)).toEventually(beTrue()) - } - - expect(invokedParameters?.attributionData.keys.contains("rc_idfa")) == true - expect(invokedParameters?.attributionData.keys.contains("rc_idfv")) == true - expect(invokedParameters?.network) == AttributionNetwork.appsFlyer - expect(invokedParameters?.appUserID) == self.purchases?.appUserID - } - - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) - func testAdServicesAttributionTokenIsAutomaticallyCollected() throws { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - throw XCTSkip("Required API is not available for this test.") - } - - setupPurchases(automaticCollection: true) - expect(self.attributionFetcher.adServicesTokenCollectionCalled) == true - } - - @available(iOS 14.3, *) - func testAdServicesAttributionTokenIsNotAutomaticallyCollectedIfDisabled() throws { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - throw XCTSkip("Required API is not available for this test.") - } - - setupPurchases(automaticCollection: false) - expect(self.attributionFetcher.adServicesTokenCollectionCalled) == false - } - - func testObserverModeSetToFalseSetFinishTransactions() throws { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - - func testDoesntFinishTransactionsIfObserverModeIsSet() throws { - try setupPurchasesObserverModeOn() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beFalse()) - } - - func testDoesntPostTransactionsIfAutoSyncPurchasesSettingIsOffInObserverMode() throws { - systemInfo = try MockSystemInfo(platformInfo: nil, - finishTransactions: false, - dangerousSettings: DangerousSettings(autoSyncPurchases: false)) - initializePurchasesInstance(appUserId: nil) - - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper( - self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beFalse()) - expect(self.storeKitWrapper.finishCalled).toEventually(beFalse()) - } - - func testDoesntPostTransactionsIfAutoSyncPurchasesSettingIsOff() throws { - systemInfo = try MockSystemInfo(platformInfo: nil, - finishTransactions: true, - dangerousSettings: DangerousSettings(autoSyncPurchases: false)) - initializePurchasesInstance(appUserId: nil) - - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beFalse()) - // Sync purchases never finishes transactions - expect(self.storeKitWrapper.finishCalled).toEventually(beFalse()) - } - - func testRestoredPurchasesArePosted() throws { - try setupPurchasesObserverModeOn() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.restored - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.storeKitWrapper.finishCalled).toEventually(beFalse()) - } - - func testNilProductIdentifier() { - setupPurchases() - let product = StoreProduct(sk1Product: SK1Product()) - var receivedError: Error? - self.purchases.purchase(product: product) { (_, _, error, _) in - receivedError = error - } - - expect(receivedError).toNot(beNil()) - } - - func testNoCrashIfPaymentIsMissing() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - } - - let transaction = SKPaymentTransaction() - - transaction.setValue(SKPaymentTransactionState.purchasing.rawValue, forKey: "transactionState") - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.setValue(SKPaymentTransactionState.purchased.rawValue, forKey: "transactionState") - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - } - - func testNoCrashIfPaymentDoesNotHaveProductIdenfier() { - setupPurchases() - - let transaction = MockTransaction() - transaction.mockPayment = SKPayment() - - transaction.setValue(SKPaymentTransactionState.purchasing.rawValue, forKey: "transactionState") - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.setValue(SKPaymentTransactionState.purchased.rawValue, forKey: "transactionState") - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - } - - func testPostsOfferingIfPurchasingPackage() throws { - setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = .success( - try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) - ) - - self.purchases!.getOfferings { (newOfferings, _) in - let package = newOfferings!["base"]!.monthly! - self.purchases!.purchase(package: package) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(CustomerInfo(testData: self.emptyCustomerInfoData)!) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedReceiptData).toNot(beNil()) - - expect(self.backend.postedProductID).to(equal(package.storeProduct.productIdentifier)) - expect(self.backend.postedPrice) == package.storeProduct.price - expect(self.backend.postedOfferingIdentifier).to(equal("base")) - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - } - - func testPurchasingPackageDoesntThrowPurchaseAlreadyInProgressIfCallbackMakesANewPurchase() throws { - setupPurchases() - var receivedError: NSError? - var secondCompletionCalled = false - mockOfferingsManager.stubbedOfferingsCompletionResult = .success( - try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) - ) - - self.purchases!.getOfferings { (newOfferings, _) in - let package = newOfferings!["base"]!.monthly! - self.purchases!.purchase(package: package) { _, _, _, _ in - self.purchases!.purchase(package: package) { (_, _, error, _) in - receivedError = error as NSError? - secondCompletionCalled = true - } - } - - self.performTransaction() - self.performTransaction() - } - expect(secondCompletionCalled).toEventually(beTrue(), timeout: .seconds(10)) - expect(receivedError).to(beNil()) - } - - func performTransaction() { - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - self.backend.postReceiptResult = .success(CustomerInfo(testData: self.emptyCustomerInfoData)!) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - } - - func testFetchCustomerInfoWhenCacheStale() { - setupPurchases() - self.deviceCache.stubbedIsCustomerInfoCacheStale = true - - self.purchases?.getCustomerInfo { (_, _) in - - } - - expect(self.backend.getSubscriberCallCount).toEventually(equal(2)) - } - - func testIsAnonymous() { - setupAnonPurchases() - expect(self.purchases.isAnonymous).to(beTrue()) - } - - func testIsNotAnonymous() { - setupPurchases() - expect(self.purchases.isAnonymous).to(beFalse()) - } - - func testProductIsRemovedButPresentInTheQueuedTransaction() throws { - self.mockProductsManager.stubbedProductsCompletionResult = Set() - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "product") - - let customerInfoBeforePurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [:] - ]]) - let customerInfoAfterPurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [product.mockProductIdentifier: []] - ]]) - self.backend.overrideCustomerInfoResult = .success(customerInfoBeforePurchase) - self.backend.postReceiptResult = .success(customerInfoAfterPurchase) - - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testReceiptsSendsObserverModeWhenObserverMode() throws { - try setupPurchasesObserverModeOn() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedObserverMode).to(beTrue()) - } - - func testReceiptsSendsObserverModeOffWhenObserverModeOff() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedObserverMode).to(beFalse()) - } - - func testInvalidateCustomerInfoCacheRemovesCachedCustomerInfo() { - setupPurchases() - guard let nonOptionalPurchases = purchases else { fatalError("failed when setting up purchases for testing") } - let appUserID = identityManager.currentAppUserID - self.deviceCache.cache(customerInfo: Data(), appUserID: appUserID) - expect(self.deviceCache.cachedCustomerInfoData(appUserID: appUserID)).toNot(beNil()) - expect(self.deviceCache.invokedClearCustomerInfoCacheCount) == 0 - - nonOptionalPurchases.invalidateCustomerInfoCache() - expect(self.deviceCache.cachedCustomerInfoData(appUserID: appUserID)).to(beNil()) - expect(self.deviceCache.invokedClearCustomerInfoCacheCount) == 1 - } - - func testGetCustomerInfoAfterInvalidatingDoesntReturnCachedVersion() throws { - setupPurchases() - guard let nonOptionalPurchases = purchases else { fatalError("failed when setting up purchases for testing") } - - let appUserID = identityManager.currentAppUserID - let oldAppUserInfo = Data() - self.deviceCache.cache(customerInfo: oldAppUserInfo, appUserID: appUserID) - let overrideCustomerInfo = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]]) - self.backend.overrideCustomerInfoResult = .success(overrideCustomerInfo) - - var receivedCustomerInfo: CustomerInfo? - var completionCallCount = 0 - var receivedError: Error? - nonOptionalPurchases.getCustomerInfo { (customerInfo, error) in - completionCallCount += 1 - receivedError = error - receivedCustomerInfo = customerInfo - } - - nonOptionalPurchases.invalidateCustomerInfoCache() - - expect(completionCallCount).toEventually(equal(1)) - expect(receivedError).to(beNil()) - expect(receivedCustomerInfo) == overrideCustomerInfo - expect(self.purchasesDelegate.customerInfoReceivedCount) == 1 - } - - func testGetCustomerInfoAfterInvalidatingCallsCompletionWithErrorIfBackendError() { - let backendError: BackendError = .networkError( - .unexpectedResponse(nil) - ) - self.backend.overrideCustomerInfoResult = .failure(backendError) - - setupPurchases() - expect(self.purchasesDelegate.customerInfoReceivedCount) == 0 - - let appUserID = identityManager.currentAppUserID - let oldAppUserInfo = Data() - self.deviceCache.cache(customerInfo: oldAppUserInfo, appUserID: appUserID) - - var receivedCustomerInfo: CustomerInfo? - var completionCallCount = 0 - var receivedError: Error? - purchases.getCustomerInfo { (customerInfo, error) in - completionCallCount += 1 - receivedError = error - receivedCustomerInfo = customerInfo - } - - purchases.invalidateCustomerInfoCache() - - expect(completionCallCount).toEventually(equal(1)) - expect(receivedError).toNot(beNil()) - expect(receivedCustomerInfo).to(beNil()) - expect(self.purchasesDelegate.customerInfoReceivedCount) == 0 - } - - func testInvalidateCustomerInfoCacheDoesntClearOfferingsCache() { - setupPurchases() - guard let nonOptionalPurchases = purchases else { fatalError("failed when setting up purchases for testing") } - - expect(self.deviceCache.clearOfferingsCacheTimestampCount) == 0 - - nonOptionalPurchases.invalidateCustomerInfoCache() - expect(self.deviceCache.clearOfferingsCacheTimestampCount) == 0 - } - - func testProxyURL() { - expect(SystemInfo.proxyURL).to(beNil()) - let defaultHostURL = URL(string: "https://api.revenuecat.com") - expect(SystemInfo.serverHostURL) == defaultHostURL - - let testURL = URL(string: "https://test_url") - Purchases.proxyURL = testURL - - expect(SystemInfo.serverHostURL) == testURL - - Purchases.proxyURL = nil - - expect(SystemInfo.serverHostURL) == defaultHostURL - } - - func testNotifiesIfTransactionIsDeferredFromStoreKit() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedError: NSError? - self.purchases.purchase(product: product) { (_, _, error, _) in - receivedError = error as NSError? - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.deferred - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beFalse()) - expect(self.storeKitWrapper.finishCalled).to(beFalse()) - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError?.domain).toEventually(equal(RCPurchasesErrorCodeDomain)) - expect(receivedError?.code).toEventually(equal(ErrorCode.paymentPendingError.rawValue)) - } - - @available(iOS 14.0, macOS 14.0, tvOS 14.0, watchOS 7.0, *) - func testSyncsPurchasesIfEntitlementsRevokedForProductIDs() throws { - try AvailabilityChecks.iOS14APIAvailableOrSkipTest() - - setupPurchases() - guard purchases != nil else { fatalError() } - expect(self.backend.postReceiptDataCalled).to(beFalse()) - (purchasesOrchestrator as StoreKitWrapperDelegate) - .storeKitWrapper(storeKitWrapper, didRevokeEntitlementsForProductIdentifiers: ["a", "b"]) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - @available(*, deprecated) // Ignore deprecation warnings - func testSetDebugLogsEnabledSetsTheCorrectValue() { - Logger.logLevel = .warn - - Purchases.debugLogsEnabled = true - expect(Logger.logLevel) == .debug - - Purchases.debugLogsEnabled = false - expect(Logger.logLevel) == .info - } - - private func verifyUpdatedCaches(newAppUserID: String) { - let expectedCallCount = 2 - expect(self.backend.getSubscriberCallCount).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.cachedCustomerInfo[newAppUserID]).toEventuallyNot(beNil()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.setCustomerInfoCacheTimestampToNowCount).toEventually(equal(expectedCallCount)) - expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(expectedCallCount)) - } - -} - -private extension OfferingsResponse { - - static let mockResponse: Self = .init( - currentOfferingId: "base", - offerings: [ - .init(identifier: "base", - description: "This is the base offering", - packages: [ - .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial") - ]) - ] - ) - -} diff --git a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift index 2ba0b5e4d8..fb24bf6130 100644 --- a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift @@ -525,7 +525,7 @@ class SubscriberAttributesManagerTests: TestCase { mockDeviceCache.stubbedUnsyncedAttributesForAllUsersResult = allAttributes let mockError: BackendError = .missingAppUserID() - mockBackend.stubbedPostSubscriberAttributesCompletionResult = (mockError, ()) + mockBackend.stubbedPostSubscriberAttributesCompletionResult = Result.failure(mockError) self.subscriberAttributesManager.syncAttributesForAllUsers(currentAppUserID: currentUserID) expect(self.mockDeviceCache.invokedDeleteAttributesIfSyncedCount).toEventually(equal(0)) From f5ed569eea0a78526224aa18c67bd00012e1c7a7 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Tue, 21 Jun 2022 21:30:19 -0500 Subject: [PATCH 03/16] Update ios 14 snapshots tets --- .../iOS14-testPostAdServicesCallsHttpClient.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json index 7a9dd746e4..d3a2b02590 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json @@ -7,6 +7,6 @@ "aad_attribution_token" : "asdf" }, "method" : "POST", - "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/asdf\/adservices_attribution" + "url" : "https://api.revenuecat.com/v1/subscribers/asdf/adservices_attribution" } } \ No newline at end of file From c4b644084126dd19552f6be28722d683cc66862b Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Tue, 21 Jun 2022 23:17:59 -0500 Subject: [PATCH 04/16] Reworked adservices to use new Purchases.shared.attribution API --- RevenueCat.xcodeproj/project.pbxproj | 1 - .../DocCDocumentation.docc/Purchases.md | 1 - .../Purchasing/Purchases/Attribution.swift | 37 ++++++++++++++++++- Sources/Purchasing/Purchases/Purchases.swift | 36 ++---------------- .../ObjCAPITester/RCAttributionAPI.m | 1 + .../ObjCAPITester/RCPurchasesAPI.m | 2 - .../SwiftAPITester/AttributionAPI.swift | 2 + .../SwiftAPITester/PurchasesAPI.swift | 3 +- .../PurchasesOrchestratorTests.swift | 8 +++- .../Purchases/BasePurchasesTests.swift | 5 ++- .../PurchasesSubscriberAttributesTests.swift | 5 ++- 11 files changed, 57 insertions(+), 44 deletions(-) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index dfc2e0e7f9..bf64ff97fb 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -1705,7 +1705,6 @@ isa = PBXGroup; children = ( 5796A39727D6C07D00653165 /* __Snapshots__ */, - 5796A38D27D6BB7D00653165 /* BackendCreateAliasTests.swift */, A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */, 5796A38927D6B96300653165 /* BackendGetCustomerInfoTests.swift */, 5796A38F27D6BCD100653165 /* BackendGetIntroEligibilityTests.swift */, diff --git a/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md b/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md index 44626a31bd..a35968285c 100644 --- a/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md +++ b/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md @@ -106,7 +106,6 @@ Most features require configuring the SDK before using it. - ``Purchases/finishTransactions`` - ``Purchases/invalidateCustomerInfoCache()`` - ``Purchases/forceUniversalAppStore`` -- ``Purchases/automaticAdServicesAttributionTokenCollection`` - ``Purchases/proxyURL`` - ``Purchases/verboseLogs`` - ``Purchases/verboseLogHandler`` diff --git a/Sources/Purchasing/Purchases/Attribution.swift b/Sources/Purchasing/Purchases/Attribution.swift index 3040025a9b..aa67893083 100644 --- a/Sources/Purchasing/Purchases/Attribution.swift +++ b/Sources/Purchasing/Purchases/Attribution.swift @@ -13,6 +13,8 @@ import Foundation +// swiftlint:disable file_length + /** * This class is responsible for all explicit attribution APIs as well as subscriber attributes that RevenueCat offers. * The attributes are additional structured information on a user. Since attributes are writable using a public key @@ -25,13 +27,18 @@ import Foundation private let subscriberAttributesManager: SubscriberAttributesManager private let currentUserProvider: CurrentUserProvider + private let attributionPoster: AttributionPoster private var appUserID: String { self.currentUserProvider.currentAppUserID } + private var automaticAdServicesAttributionTokenCollection: Bool = false weak var delegate: AttributionDelegate? - init(subscriberAttributesManager: SubscriberAttributesManager, currentUserProvider: CurrentUserProvider) { + init(subscriberAttributesManager: SubscriberAttributesManager, + currentUserProvider: CurrentUserProvider, + attributionPoster: AttributionPoster) { self.subscriberAttributesManager = subscriberAttributesManager self.currentUserProvider = currentUserProvider + self.attributionPoster = attributionPoster super.init() @@ -40,6 +47,34 @@ import Foundation } +public extension Attribution { + + /** + * Enable automatic collection of AdServices attribution token. + * + * Should match OS availability in https://developer.apple.com/documentation/ad_services + */ + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @objc func enableAdServicesAttributionTokenCollection() { + self.automaticAdServicesAttributionTokenCollection = true + self.postAdServicesTokenIfNeeded() + } + + // should match OS availability in https://developer.apple.com/documentation/ad_services + @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + internal func postAdServicesTokenIfNeeded() { + guard self.automaticAdServicesAttributionTokenCollection else { + return + } + attributionPoster.postAdServicesTokenIfNeeded() + } + +} + public extension Attribution { /** diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6681e1fdab..1b112d21a5 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -91,16 +91,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void private weak var privateDelegate: PurchasesDelegate? private let operationDispatcher: OperationDispatcher - /** - * Enable automatic collection of AdServices attribution token. Defaults to `false`. - * - * Should match OS availability in https://developer.apple.com/documentation/ad_services - */ - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @objc public static var automaticAdServicesAttributionTokenCollection: Bool = false - /** * Used to set the log level. Useful for debugging issues with the lovely team @RevenueCat. * @@ -312,13 +302,14 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void operationDispatcher: operationDispatcher, attributionFetcher: attributionFetcher, attributionDataMigrator: attributionDataMigrator) - let subscriberAttributes = Attribution(subscriberAttributesManager: subscriberAttributesManager, - currentUserProvider: identityManager) let attributionPoster = AttributionPoster(deviceCache: deviceCache, currentUserProvider: identityManager, backend: backend, attributionFetcher: attributionFetcher, subscriberAttributesManager: subscriberAttributesManager) + let subscriberAttributes = Attribution(subscriberAttributesManager: subscriberAttributesManager, + currentUserProvider: identityManager, + attributionPoster: attributionPoster) let productsRequestFactory = ProductsRequestFactory() let productsManager = ProductsManager(productsRequestFactory: productsRequestFactory, systemInfo: systemInfo, @@ -402,7 +393,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void trialOrIntroPriceEligibilityChecker: trialOrIntroPriceChecker) } - // swiftlint:disable:next function_body_length init(appUserID: String?, requestFetcher: StoreKitRequestFetcher, receiptFetcher: ReceiptFetcher, @@ -470,13 +460,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void subscribeToAppStateNotifications() attributionPoster.postPostponedAttributionDataIfNeeded() -#if os(iOS) || os(macOS) - // should match OS availability in https://developer.apple.com/documentation/ad_services - if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - postAdServicesTokenIfNeeded() - } -#endif - self.customerInfoObservationDisposable = customerInfoManager.monitorChanges { [weak self] customerInfo in guard let self = self else { return } self.delegate?.purchases?(self, receivedUpdated: customerInfo) @@ -517,17 +500,6 @@ extension Purchases { attributionPoster.post(attributionData: data, fromNetwork: network, networkUserId: networkUserId) } - // should match OS availability in https://developer.apple.com/documentation/ad_services - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - private func postAdServicesTokenIfNeeded() { - guard Self.automaticAdServicesAttributionTokenCollection else { - return - } - attributionPoster.postAdServicesTokenIfNeeded() - } - } // MARK: Identity @@ -1668,7 +1640,7 @@ private extension Purchases { #if os(iOS) || os(macOS) // should match OS availability in https://developer.apple.com/documentation/ad_services if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - postAdServicesTokenIfNeeded() + self.attribution.postAdServicesTokenIfNeeded() } #endif } diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionAPI.m index a33ab8985f..e3b4a9615d 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCAttributionAPI.m @@ -53,6 +53,7 @@ + (void)checkAPI { [a setCreative: nil]; [a setCreative: @""]; [a collectDeviceIdentifiers]; + [a enableAdServicesAttributionTokenCollection]; } @end diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 6b46c63a95..47d508b5f3 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -80,8 +80,6 @@ + (void)checkAPI { // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use Purchases.automaticAdServicesAttributionTokenCollection instead automaticAppleSearchAdsAttributionCollection = [RCPurchases automaticAppleSearchAdsAttributionCollection]; - automaticAdServicesAttributionTokenCollection = [RCPurchases automaticAdServicesAttributionTokenCollection]; - // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead debugLogsEnabled = [RCPurchases debugLogsEnabled]; diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionAPI.swift index 805deb26da..8a6c6f4d7b 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/AttributionAPI.swift @@ -71,4 +71,6 @@ func checkAttributionAPI() { attribution.setCreative(nil) attribution.collectDeviceIdentifiers() + + attribution.enableAdServicesAttributionTokenCollection() } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 197008c0e1..052ef615f1 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -86,10 +86,9 @@ private func checkStaticMethods() { let simulatesAskToBuyInSandbox: Bool = Purchases.simulatesAskToBuyInSandbox let sharedPurchases: Purchases = Purchases.shared let isPurchasesConfigured: Bool = Purchases.isConfigured - let automaticAdServicesAttributionTokenCollection: Bool = Purchases.automaticAdServicesAttributionTokenCollection print(canI, version, logLevel, proxyUrl!, forceUniversalAppStore, simulatesAskToBuyInSandbox, - sharedPurchases, isPurchasesConfigured, automaticAdServicesAttributionTokenCollection) + sharedPurchases, isPurchasesConfigured) } private func checkTypealiases( diff --git a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift index 2ae837e4d5..077a7e4ca1 100644 --- a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift +++ b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -65,8 +65,14 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { operationDispatcher: MockOperationDispatcher(), attributionFetcher: attributionFetcher, attributionDataMigrator: MockAttributionDataMigrator()) + let attributionPoster = AttributionPoster(deviceCache: deviceCache, + currentUserProvider: currentUserProvider, + backend: backend, + attributionFetcher: attributionFetcher, + subscriberAttributesManager: subscriberAttributesManager) attribution = Attribution(subscriberAttributesManager: subscriberAttributesManager, - currentUserProvider: MockCurrentUserProvider(mockAppUserID: mockUserID)) + currentUserProvider: MockCurrentUserProvider(mockAppUserID: mockUserID), + attributionPoster: attributionPoster) mockManageSubsHelper = MockManageSubscriptionsHelper(systemInfo: systemInfo, customerInfoManager: customerInfoManager, currentUserProvider: currentUserProvider) diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index b5490e84e8..f77b926fb7 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -56,13 +56,14 @@ class BasePurchasesTests: TestCase { attributionFetcher: self.attributionFetcher, attributionDataMigrator: AttributionDataMigrator() ) - self.attribution = Attribution(subscriberAttributesManager: self.subscriberAttributesManager, - currentUserProvider: self.identityManager) self.attributionPoster = AttributionPoster(deviceCache: self.deviceCache, currentUserProvider: self.identityManager, backend: self.backend, attributionFetcher: self.attributionFetcher, subscriberAttributesManager: self.subscriberAttributesManager) + self.attribution = Attribution(subscriberAttributesManager: self.subscriberAttributesManager, + currentUserProvider: self.identityManager, + attributionPoster: self.attributionPoster) self.customerInfoManager = CustomerInfoManager(operationDispatcher: self.mockOperationDispatcher, deviceCache: self.deviceCache, backend: self.backend, diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index 2692e1126c..d4072deae1 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -94,13 +94,14 @@ class PurchasesSubscriberAttributesTests: TestCase { attributionDataMigrator: AttributionDataMigrator() ) self.mockIdentityManager = MockIdentityManager(mockAppUserID: "app_user") - self.attribution = Attribution(subscriberAttributesManager: self.mockSubscriberAttributesManager, - currentUserProvider: self.mockIdentityManager) self.mockAttributionPoster = AttributionPoster(deviceCache: mockDeviceCache, currentUserProvider: mockIdentityManager, backend: mockBackend, attributionFetcher: mockAttributionFetcher, subscriberAttributesManager: mockSubscriberAttributesManager) + self.attribution = Attribution(subscriberAttributesManager: self.mockSubscriberAttributesManager, + currentUserProvider: self.mockIdentityManager, + attributionPoster: self.mockAttributionPoster) self.customerInfoManager = CustomerInfoManager(operationDispatcher: mockOperationDispatcher, deviceCache: mockDeviceCache, backend: mockBackend, From efa22dffc2e001edff34d7ccc93d34be06b9be77 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Wed, 22 Jun 2022 20:36:11 -0500 Subject: [PATCH 05/16] Updated with some comments from PR --- Sources/Networking/Backend.swift | 1 + Sources/Purchasing/Purchases/Attribution.swift | 2 -- Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m | 1 - .../SubscriberAttributes/SubscriberAttributesManagerTests.swift | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index cb99b6a1c8..cb537a11f8 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -94,6 +94,7 @@ class Backend { completion: CustomerAPI.SimpleResponseHandler?) { self.customer.post(subscriberAttributes: subscriberAttributes, appUserID: appUserID, completion: completion) } + } extension Backend { diff --git a/Sources/Purchasing/Purchases/Attribution.swift b/Sources/Purchasing/Purchases/Attribution.swift index aa67893083..f6c4a20372 100644 --- a/Sources/Purchasing/Purchases/Attribution.swift +++ b/Sources/Purchasing/Purchases/Attribution.swift @@ -51,8 +51,6 @@ public extension Attribution { /** * Enable automatic collection of AdServices attribution token. - * - * Should match OS availability in https://developer.apple.com/documentation/ad_services */ @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) @available(tvOS, unavailable) diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 47d508b5f3..f2efbd4d6b 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -16,7 +16,6 @@ @implementation RCPurchasesAPI NSString *version; BOOL automaticAppleSearchAdsAttributionCollection; -BOOL automaticAdServicesAttributionTokenCollection; BOOL debugLogsEnabled; RCLogLevel logLevel; NSURL *proxyURL; diff --git a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift index d764e476a0..0a08b2f8ec 100644 --- a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift @@ -525,7 +525,7 @@ class SubscriberAttributesManagerTests: TestCase { mockDeviceCache.stubbedUnsyncedAttributesForAllUsersResult = allAttributes let mockError: BackendError = .missingAppUserID() - mockBackend.stubbedPostSubscriberAttributesCompletionResult = Result.failure(mockError) + mockBackend.stubbedPostSubscriberAttributesCompletionResult = .failure(mockError) self.subscriberAttributesManager.syncAttributesForAllUsers(currentAppUserID: currentUserID) expect(self.mockDeviceCache.invokedDeleteAttributesIfSyncedCount).toEventually(equal(0)) From 45ace53918f9847368025a7bc37acc1c4c72f863 Mon Sep 17 00:00:00 2001 From: beylmk Date: Mon, 27 Jun 2022 18:20:44 -0700 Subject: [PATCH 06/16] Add iAd code back (#1739) Co-authored-by: NachoSoto --- RevenueCat.xcodeproj/project.pbxproj | 12 + Sources/Attribution/AfficheClientProxy.swift | 62 ++++ Sources/Attribution/AttributionFetcher.swift | 29 ++ Sources/Attribution/AttributionNetwork.swift | 11 + Sources/Attribution/AttributionPoster.swift | 89 +++++- .../Attribution/AttributionTypeFactory.swift | 4 + Sources/Misc/Deprecations.swift | 2 +- Sources/Networking/Backend.swift | 10 + Sources/Networking/CustomerAPI.swift | 13 + .../PostAttributionDataOperation.swift | 75 +++++ .../Purchasing/Purchases/Attribution.swift | 2 +- Sources/Purchasing/Purchases/Purchases.swift | 30 +- .../ObjCAPITester/RCPurchasesAPI.m | 2 +- .../SwiftAPITester/PurchasesAPI.swift | 2 +- .../Attribution/AttributionPosterTests.swift | 279 ++++++++++++++---- .../Mocks/MockAttributionFetcher.swift | 1 + .../Mocks/MockAttributionTypeFactory.swift | 25 ++ Tests/UnitTests/Mocks/MockBackend.swift | 21 ++ .../BackendPostAttributionDataTests.swift | 42 +++ ...testPostAttributesPutsDataInDataKey.1.json | 2 +- ...testPostAttributesPutsDataInDataKey.1.json | 2 +- ...testPostAttributesPutsDataInDataKey.1.json | 2 +- ...testPostAttributesPutsDataInDataKey.1.json | 2 +- .../Purchases/BasePurchasesTests.swift | 28 ++ .../PurchasesAttributionDataTests.swift | 91 ++++++ .../Purchasing/PurchasesDeprecation.swift | 8 + .../PurchasesSubscriberAttributesTests.swift | 4 +- 27 files changed, 764 insertions(+), 86 deletions(-) create mode 100644 Sources/Attribution/AfficheClientProxy.swift create mode 100644 Sources/Networking/Operations/PostAttributionDataOperation.swift create mode 100644 Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 663faadde5..66d1f43110 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -349,6 +349,9 @@ A563F586271E072B00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; A563F589271E1DAD00246E0C /* MockBeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */; }; A56C2E012819C33500995421 /* BackendPostAdServicesTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */; }; + A56DFDEC2866438B00EF2E32 /* AfficheClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56DFDEB2866438B00EF2E32 /* AfficheClientProxy.swift */; }; + A56DFDF0286643BF00EF2E32 /* PostAttributionDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56DFDEF286643BF00EF2E32 /* PostAttributionDataOperation.swift */; }; + A56DFDF3286673CE00EF2E32 /* BackendPostAttributionDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56DFDF1286665E600EF2E32 /* BackendPostAttributionDataTests.swift */; }; A56F9AB126990E9200AFC48F /* CustomerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56F9AB026990E9200AFC48F /* CustomerInfo.swift */; }; A5B6CDD8280F3843007629D5 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5B6CDD5280F3843007629D5 /* AdServices.framework */; platformFilters = (ios, maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; }; A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */; }; @@ -828,6 +831,9 @@ A563F585271E072B00246E0C /* MockBeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBeginRefundRequestHelper.swift; sourceTree = ""; }; A563F587271E076800246E0C /* BeginRefundRequestHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelperTests.swift; sourceTree = ""; }; A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostAdServicesTokenTests.swift; sourceTree = ""; }; + A56DFDEB2866438B00EF2E32 /* AfficheClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AfficheClientProxy.swift; sourceTree = ""; }; + A56DFDEF286643BF00EF2E32 /* PostAttributionDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAttributionDataOperation.swift; sourceTree = ""; }; + A56DFDF1286665E600EF2E32 /* BackendPostAttributionDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendPostAttributionDataTests.swift; sourceTree = ""; }; A56F9AB026990E9200AFC48F /* CustomerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfo.swift; sourceTree = ""; }; A5B6CDD5280F3843007629D5 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/AdServices.framework; sourceTree = DEVELOPER_DIR; }; A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelper.swift; sourceTree = ""; }; @@ -1718,6 +1724,7 @@ 5796A39827D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift */, 5796A38727D6B85900653165 /* BackendPostReceiptDataTests.swift */, 5796A38027D6B78500653165 /* BaseBackendTest.swift */, + A56DFDF1286665E600EF2E32 /* BackendPostAttributionDataTests.swift */, ); path = Backend; sourceTree = ""; @@ -1785,6 +1792,7 @@ B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */, B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */, A55D5D65282ECCC100FA7623 /* PostAdServicesTokenOperation.swift */, + A56DFDEF286643BF00EF2E32 /* PostAttributionDataOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1887,6 +1895,7 @@ B3A55B7C26C452A7007EFC56 /* AttributionPoster.swift */, F5BE44422698581100254A30 /* AttributionTypeFactory.swift */, F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */, + A56DFDEB2866438B00EF2E32 /* AfficheClientProxy.swift */, ); path = Attribution; sourceTree = ""; @@ -2403,6 +2412,7 @@ 35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */, A55D08302722368600D919E0 /* SK2BeginRefundRequestHelper.swift in Sources */, 35D832CD262A5B7500E60AC5 /* ETagManager.swift in Sources */, + A56DFDF0286643BF00EF2E32 /* PostAttributionDataOperation.swift in Sources */, 2DDF41BC24F6F392005BC22D /* ArraySlice_UInt8+Extensions.swift in Sources */, 574A2F4B282D7AEA00150D40 /* PostOfferResponse.swift in Sources */, F575858D26C088FE00C12B97 /* OfferingsManager.swift in Sources */, @@ -2436,6 +2446,7 @@ B302206E2728B798008F1A0D /* BackendErrorStrings.swift in Sources */, 2D8D03B52799A2B90044C2ED /* DocCDocumentation.docc in Sources */, 576C8A8B27CFCB150058FA6E /* AnyEncodable.swift in Sources */, + A56DFDEC2866438B00EF2E32 /* AfficheClientProxy.swift in Sources */, 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */, B372EC54268FEDC60099171E /* StoreProductDiscount.swift in Sources */, B34605BE279A6E380031CA74 /* OfferingsCallback.swift in Sources */, @@ -2508,6 +2519,7 @@ B3CAFF10285CE8E30048A994 /* MockOfferingsAPI.swift in Sources */, 351B51BF26D450E800BD2BD7 /* StoreKitRequestFetcherTests.swift in Sources */, 2DDF41E224F6F527005BC22D /* MockProductsRequest.swift in Sources */, + A56DFDF3286673CE00EF2E32 /* BackendPostAttributionDataTests.swift in Sources */, 351B514B26D44A4A00BD2BD7 /* MockOperationDispatcher.swift in Sources */, 351B514926D44A2F00BD2BD7 /* MockCustomerInfoManager.swift in Sources */, 5766AAC9283E88CF00FA6091 /* PurchasesGetCustomerInfoTests.swift in Sources */, diff --git a/Sources/Attribution/AfficheClientProxy.swift b/Sources/Attribution/AfficheClientProxy.swift new file mode 100644 index 0000000000..81aff940d5 --- /dev/null +++ b/Sources/Attribution/AfficheClientProxy.swift @@ -0,0 +1,62 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// AfficheClientProxy.swift +// +// Created by Juanpe Catalán on 14/7/21. +// + +import Foundation + +typealias AttributionDetailsBlock = ([String: NSObject]?, Error?) -> Void + +// We need this class to avoid Kid apps being rejected for getting idfa. It seems like App +// Review uses some grep to find the class names, so we ended up creating a fake class that +// exposes the same methods we're looking for in ADClient to call the same methods and mangling +// the class names. So that Apple can't find them during the review, but we can still access them on runtime. +// You can see the class here: https://rev.cat/fake-affiche-client +class FakeAfficheClient: NSObject { + + // We need this method to be available as an optional implicitly unwrapped method for `AnyClass`. + @objc static func sharedClient() -> FakeAfficheClient { + FakeAfficheClient() + } + + // We need this method to be available as an optional implicitly unwrapped method for `AnyClass`. + @objc(requestAttributionDetailsWithBlock:) + func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { + Logger.warn(Strings.attribution.apple_affiche_framework_present_but_couldnt_call_request_attribution_details) + } + +} + +class AfficheClientProxy { + + private static let mangledClassName = "NQPyvrag" + + static var afficheClientClass: AnyClass? { + NSClassFromString(Self.mangledClassName.rot13()) + } + + func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { + let client: AnyObject + if let klass = Self.afficheClientClass, let clientClass = klass as AnyObject as? NSObjectProtocol { + // This looks strange, but #selector() does fun things to create a selector. If the selector for the given + // function matches the selector on another class, it can be used in place. Results: + // If ADClient class is instantiated above, then +sharedClient selector is performed even though you can see + // that we're using #selector(FakeAfficheClient.sharedClient) to instantiate a Selector object. + client = clientClass.perform(#selector(FakeAfficheClient.sharedClient)).takeUnretainedValue() + } else { + client = FakeAfficheClient.sharedClient() + } + + client.requestAttributionDetails(completionHandler) + } + +} diff --git a/Sources/Attribution/AttributionFetcher.swift b/Sources/Attribution/AttributionFetcher.swift index 8b9b90dadb..4d58f19342 100644 --- a/Sources/Attribution/AttributionFetcher.swift +++ b/Sources/Attribution/AttributionFetcher.swift @@ -73,6 +73,21 @@ class AttributionFetcher { return nil } + func afficheClientAttributionDetails(completion: @escaping ([String: NSObject]?, Error?) -> Void) { + // Should match available platforms in + // https://developer.apple.com/documentation/iad/adclient?language=swift +#if os(iOS) + guard let afficheClientProxy = attributionFactory.afficheClientProxy() else { + Logger.warn(Strings.attribution.search_ads_attribution_cancelled_missing_ad_framework) + completion(nil, AttributionFetcherError.identifierForAdvertiserFrameworksUnavailable) + return + } + afficheClientProxy.requestAttributionDetails(completion) +#else + completion(nil, AttributionFetcherError.identifierForAdvertiserUnavailableForPlatform) +#endif + } + // should match OS availability in https://developer.apple.com/documentation/ad_services @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) var adServicesToken: String? { @@ -90,6 +105,20 @@ class AttributionFetcher { #endif } + var isAuthorizedToPostSearchAds: Bool { + // Should match platforms that require permissions detailed in + // https://developer.apple.com/app-store/user-privacy-and-data-use/ + if !appTrackingTransparencyRequired { + return true + } + + if #available(iOS 14.0.0, tvOS 14.0.0, *) { + return isAuthorizedToPostSearchAdsInATTRequiredOS + } + + return true + } + var authorizationStatus: FakeTrackingManagerAuthorizationStatus { // should match OS availability here: https://rev.cat/app-tracking-transparency guard #available(iOS 14.0.0, tvOS 14.0.0, macOS 11.0.0, *) else { diff --git a/Sources/Attribution/AttributionNetwork.swift b/Sources/Attribution/AttributionNetwork.swift index dadb146d71..52a9af04dd 100644 --- a/Sources/Attribution/AttributionNetwork.swift +++ b/Sources/Attribution/AttributionNetwork.swift @@ -70,3 +70,14 @@ extension AttributionNetwork: Encodable { } } + +extension AttributionNetwork { + + var isAppleSearchAdds: Bool { + switch self { + case .appleSearchAds: return true + default: return false + } + } + +} diff --git a/Sources/Attribution/AttributionPoster.swift b/Sources/Attribution/AttributionPoster.swift index 8556e46416..ad0c288610 100644 --- a/Sources/Attribution/AttributionPoster.swift +++ b/Sources/Attribution/AttributionPoster.swift @@ -53,19 +53,13 @@ class AttributionPoster { } let currentAppUserID = self.currentUserProvider.currentAppUserID - let latestNetworkIdsAndAdvertisingIdsSentByNetwork = - deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: currentAppUserID) - let latestSentToNetwork = latestNetworkIdsAndAdvertisingIdsSentByNetwork[network] - - let newValueForNetwork = "\(identifierForAdvertisers ?? "(null)")_\(networkUserId ?? "(null)")" - guard latestSentToNetwork != newValueForNetwork else { - Logger.debug(Strings.attribution.skip_same_attributes) + guard let newDictToCache = getNewDictToCache(currentAppUserID: currentAppUserID, + idfa: identifierForAdvertisers, + network: network, + networkUserId: networkUserId) else { return } - var newDictToCache = latestNetworkIdsAndAdvertisingIdsSentByNetwork - newDictToCache[network] = newValueForNetwork - var newData = data if let identifierForAdvertisers = identifierForAdvertisers { @@ -87,10 +81,45 @@ class AttributionPoster { } if !newData.isEmpty { - postSubscriberAttributes(newData: newData, - network: network, - appUserID: currentAppUserID, - newDictToCache: newDictToCache) + if network.isAppleSearchAdds { + postSearchAds(newData: newData, + network: network, + appUserID: currentAppUserID, + newDictToCache: newDictToCache) + } else { + postSubscriberAttributes(newData: newData, + network: network, + appUserID: currentAppUserID, + newDictToCache: newDictToCache) + } + } + } + + @available(*, deprecated) + func postAppleSearchAdsAttributionIfNeeded() { + guard attributionFetcher.isAuthorizedToPostSearchAds else { + return + } + + guard self.latestNetworkIdAndAdvertisingIdentifierSent(network: .appleSearchAds) == nil else { + return + } + + attributionFetcher.afficheClientAttributionDetails { attributionDetails, error in + guard let attributionDetails = attributionDetails, + error == nil else { + return + } + + let attributionDetailsValues = Array(attributionDetails.values) + let firstAttributionDict = attributionDetailsValues.first as? [String: NSObject] + + guard let hasIad = firstAttributionDict?["iad-attribution"] as? NSNumber, + hasIad.boolValue == true else { + return + } + + self.post(attributionData: attributionDetails, fromNetwork: .appleSearchAds, networkUserId: nil) } } @@ -163,6 +192,19 @@ class AttributionPoster { return cachedDict[network] } + private func postSearchAds(newData: [String: Any], + network: AttributionNetwork, + appUserID: String, + newDictToCache: [AttributionNetwork: String]) { + backend.post(attributionData: newData, network: network, appUserID: appUserID) { error in + guard error == nil else { + return + } + + self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: appUserID) + } + } + private func postSubscriberAttributes(newData: [String: Any], network: AttributionNetwork, appUserID: String, @@ -173,4 +215,23 @@ class AttributionPoster { deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: appUserID) } + private func getNewDictToCache(currentAppUserID: String, + idfa: String?, + network: AttributionNetwork, + networkUserId: String?) -> [AttributionNetwork: String]? { + let latestAdvertisingIdsByNetworkSent = + deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: currentAppUserID) + let latestSentToNetwork = latestAdvertisingIdsByNetworkSent[network] + + let newValueForNetwork = "\(idfa ?? "(null)")_\(networkUserId ?? "(null)")" + guard latestSentToNetwork != newValueForNetwork else { + Logger.debug(Strings.attribution.skip_same_attributes) + return nil + } + + var newDictToCache = latestAdvertisingIdsByNetworkSent + newDictToCache[network] = newValueForNetwork + return newDictToCache + } + } diff --git a/Sources/Attribution/AttributionTypeFactory.swift b/Sources/Attribution/AttributionTypeFactory.swift index ceb0697559..70717f241a 100644 --- a/Sources/Attribution/AttributionTypeFactory.swift +++ b/Sources/Attribution/AttributionTypeFactory.swift @@ -16,6 +16,10 @@ import Foundation class AttributionTypeFactory { + func afficheClientProxy() -> AfficheClientProxy? { + return AfficheClientProxy.afficheClientClass == nil ? nil : AfficheClientProxy() + } + func atFollowingProxy() -> TrackingManagerProxy? { return TrackingManagerProxy.trackingClass == nil ? nil : TrackingManagerProxy() } diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index 163723827d..4bd639edc3 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -146,7 +146,7 @@ public extension Purchases { /** * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. */ - @available(*, deprecated, message: "Use Purchases.automaticAdServicesAttributionTokenCollection instead") + @available(*, deprecated, message: "Use Purchases.attribution.enableAdServicesAttributionTokenCollection() instead") @objc static var automaticAppleSearchAdsAttributionCollection: Bool = false } diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index cb537a11f8..2ba308985d 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -58,6 +58,16 @@ class Backend { self.config.clearCache() } + func post(attributionData: [String: Any], + network: AttributionNetwork, + appUserID: String, + completion: CustomerAPI.SimpleResponseHandler?) { + self.customer.post(attributionData: attributionData, + network: network, + appUserID: appUserID, + completion: completion) + } + func post(adServicesToken: String, appUserID: String, completion: CustomerAPI.SimpleResponseHandler?) { diff --git a/Sources/Networking/CustomerAPI.swift b/Sources/Networking/CustomerAPI.swift index dc88689318..bd2d29c7ef 100644 --- a/Sources/Networking/CustomerAPI.swift +++ b/Sources/Networking/CustomerAPI.swift @@ -51,6 +51,19 @@ class CustomerAPI { self.backendConfig.operationQueue.addOperation(operation) } + func post(attributionData: [String: Any], + network: AttributionNetwork, + appUserID: String, + completion: SimpleResponseHandler?) { + let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient, + appUserID: appUserID) + let postAttributionDataOperation = PostAttributionDataOperation(configuration: config, + attributionData: attributionData, + network: network, + responseHandler: completion) + self.backendConfig.operationQueue.addOperation(postAttributionDataOperation) + } + func post(adServicesToken: String, appUserID: String, completion: SimpleResponseHandler?) { diff --git a/Sources/Networking/Operations/PostAttributionDataOperation.swift b/Sources/Networking/Operations/PostAttributionDataOperation.swift new file mode 100644 index 0000000000..4187547e99 --- /dev/null +++ b/Sources/Networking/Operations/PostAttributionDataOperation.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 +// +// PostAttributionDataOperation.swift +// +// Created by Joshua Liebowitz on 11/19/21. + +import Foundation + +class PostAttributionDataOperation: NetworkOperation { + + private let configuration: UserSpecificConfiguration + private let attributionData: [String: Any] + private let network: AttributionNetwork + private let responseHandler: CustomerAPI.SimpleResponseHandler? + + init(configuration: UserSpecificConfiguration, + attributionData: [String: Any], + network: AttributionNetwork, + responseHandler: CustomerAPI.SimpleResponseHandler?) { + self.attributionData = attributionData + self.network = network + self.configuration = configuration + self.responseHandler = responseHandler + + super.init(configuration: configuration) + } + + override func begin(completion: @escaping () -> Void) { + self.post(completion: completion) + } + + private func post(completion: @escaping () -> Void) { + guard let appUserID = try? self.configuration.appUserID.escapedOrError() else { + self.responseHandler?(.missingAppUserID()) + completion() + + return + } + + let request = HTTPRequest(method: .post(Body(network: self.network, attributionData: self.attributionData)), + path: .postAttributionData(appUserID: appUserID)) + + self.httpClient.perform(request) { (response: HTTPResponse.Result) in + defer { + completion() + } + + self.responseHandler?(response.error.map(BackendError.networkError)) + } + } + +} + +private extension PostAttributionDataOperation { + + struct Body: Encodable { + + let network: AttributionNetwork + let data: AnyEncodable + + init(network: AttributionNetwork, attributionData: [String: Any]) { + self.network = network + self.data = AnyEncodable(attributionData) + } + + } + +} diff --git a/Sources/Purchasing/Purchases/Attribution.swift b/Sources/Purchasing/Purchases/Attribution.swift index 8fed3f40dd..09e7b79350 100644 --- a/Sources/Purchasing/Purchases/Attribution.swift +++ b/Sources/Purchasing/Purchases/Attribution.swift @@ -68,7 +68,7 @@ public extension Attribution { guard self.automaticAdServicesAttributionTokenCollection else { return } - attributionPoster.postAdServicesTokenIfNeeded() + self.attributionPoster.postAdServicesTokenIfNeeded() } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 0deff7358e..458a675f53 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -458,8 +458,11 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void if self.systemInfo.dangerousSettings.autoSyncPurchases { storeKitWrapper.delegate = purchasesOrchestrator } - subscribeToAppStateNotifications() - attributionPoster.postPostponedAttributionDataIfNeeded() + + self.subscribeToAppStateNotifications() + self.attributionPoster.postPostponedAttributionDataIfNeeded() + + (self as DeprecatedSearchAdsAttribution).postAppleSearchAddsAttributionCollectionIfNeeded() self.customerInfoObservationDisposable = customerInfoManager.monitorChanges { [weak self] customerInfo in guard let self = self else { return } @@ -472,7 +475,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void storeKitWrapper.delegate = nil customerInfoObservationDisposable?() privateDelegate = nil - Self.proxyURL = nil } @@ -501,6 +503,14 @@ extension Purchases { attributionPoster.post(attributionData: data, fromNetwork: network, networkUserId: networkUserId) } + @available(*, deprecated) + fileprivate func postAppleSearchAddsAttributionCollectionIfNeeded() { + guard Self.automaticAppleSearchAdsAttributionCollection else { + return + } + attributionPoster.postAppleSearchAdsAttributionIfNeeded() + } + } // MARK: Identity @@ -1644,8 +1654,9 @@ private extension Purchases { self.updateAllCachesIfNeeded() self.dispatchSyncSubscriberAttributes() + (self as DeprecatedSearchAdsAttribution).postAppleSearchAddsAttributionCollectionIfNeeded() + #if os(iOS) || os(macOS) - // should match OS availability in https://developer.apple.com/documentation/ad_services if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { self.attribution.postAdServicesTokenIfNeeded() } @@ -1702,3 +1713,14 @@ private extension Purchases { } } + +// MARK: - Deprecations + +/// Protocol to be able to call `Purchases.postAppleSearchAddsAttributionCollectionIfNeeded` without warnings +private protocol DeprecatedSearchAdsAttribution { + + func postAppleSearchAddsAttributionCollectionIfNeeded() + +} + +extension Purchases: DeprecatedSearchAdsAttribution {} diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index f2efbd4d6b..74e7ed564f 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -76,7 +76,7 @@ + (void)checkAPI { [RCPurchases addAttributionData:@{} fromNetwork:RCAttributionNetworkBranch forNetworkUserId:nil]; // should have deprecation warning: - // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use Purchases.automaticAdServicesAttributionTokenCollection instead + // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use Purchases.attribution.enableAdServicesAttributionTokenCollection instead automaticAppleSearchAdsAttributionCollection = [RCPurchases automaticAppleSearchAdsAttributionCollection]; // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 052ef615f1..eae70565b7 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -79,7 +79,6 @@ private func checkStaticMethods() { let canI: Bool = Purchases.canMakePayments() let version = Purchases.frameworkVersion - let logLevel: LogLevel = Purchases.logLevel let proxyUrl: URL? = Purchases.proxyURL let forceUniversalAppStore: Bool = Purchases.forceUniversalAppStore @@ -264,6 +263,7 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: "") Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: nil) let _: Bool = Purchases.automaticAppleSearchAdsAttributionCollection + Purchases.automaticAppleSearchAdsAttributionCollection = false purchases.checkTrialOrIntroDiscountEligibility([String]()) { (_: [String: IntroEligibility]) in } diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index 8d3617a02e..725439e69b 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -55,118 +55,279 @@ class BaseAttributionPosterTests: TestCase { attributionFetcher: self.attributionFetcher, subscriberAttributesManager: self.subscriberAttributesManager) self.resetAttributionStaticProperties() + self.backend.stubbedPostAttributionDataCompletionResult = (nil, ()) self.backend.stubbedPostAdServicesTokenCompletionResult = .success(()) } - override func tearDown() { - UserDefaults.standard.removePersistentDomain(forName: userDefaultsSuiteName) - UserDefaults.standard.synchronize() - resetAttributionStaticProperties() - super.tearDown() - } - private func resetAttributionStaticProperties() { if #available(iOS 14, macOS 11, tvOS 14, *) { MockTrackingManagerProxy.mockAuthorizationStatus = .authorized } - + MockAttributionTypeFactory.shouldReturnAdClientProxy = true MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + MockAdClientProxy.requestAttributionDetailsCallCount = 0 } -} - -#if canImport(AdServices) -@available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) -class AdServicesAttributionPosterTests: BaseAttributionPosterTests { - - func testPostAdServicesTokenIfNeededSkipsIfAlreadySent() { - backend.stubbedPostAdServicesTokenCompletionResult = .success(()) - - attributionPoster.postAdServicesTokenIfNeeded() - expect(self.backend.invokedPostAdServicesTokenCount) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 - - attributionPoster.postAdServicesTokenIfNeeded() - expect(self.backend.invokedPostAdServicesTokenCount) == 1 - expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - } - - func testPostAdServicesTokenIfNeededSkipsIfNilToken() throws { - backend.stubbedPostAdServicesTokenCompletionResult = .success(()) - - attributionFetcher.adServicesTokenToReturn = nil - attributionPoster.postAdServicesTokenIfNeeded() - expect(self.backend.invokedPostAdServicesTokenCount) == 0 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - } - - func testPostAdServicesTokenIfNeededDoesNotCacheOnAPIError() throws { - let stubbedError: BackendError = .networkError( - .errorResponse(.init(code: .invalidAPIKey, message: nil), - 400) - ) - - backend.stubbedPostAdServicesTokenCompletionResult = .failure(stubbedError) - - attributionFetcher.adServicesTokenToReturn = nil - attributionPoster.postAdServicesTokenIfNeeded() - expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 0 + override func tearDown() { + UserDefaults.standard.removePersistentDomain(forName: userDefaultsSuiteName) + UserDefaults.standard.synchronize() + resetAttributionStaticProperties() + super.tearDown() } } -#endif class AttributionPosterTests: BaseAttributionPosterTests { func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" - + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) - - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: userID) - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 } + @available(*, deprecated) + func testPostAppleSearchAdsAttributionDataSkipsIfAlreadySent() { + let userID = "userID" + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) + + attributionPoster.post(attributionData: ["something": "here"], + fromNetwork: .appleSearchAds, + networkUserId: userID) + expect(self.backend.invokedPostAttributionDataCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + + attributionPoster.post(attributionData: ["something": "else"], + fromNetwork: .appleSearchAds, + networkUserId: userID) + expect(self.backend.invokedPostAttributionDataCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + func testPostAttributionDataDoesntSkipIfNetworkChanged() { let userID = "userID" - backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) + attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: userID) - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .facebook, networkUserId: userID) - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } + @available(*, deprecated) + func testPostAppleSearchAdsAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) + + attributionPoster.post(attributionData: ["something": "here"], + fromNetwork: .appleSearchAds, + networkUserId: "attributionUser1") + expect(self.backend.invokedPostAttributionDataCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + + attributionPoster.post(attributionData: ["something": "else"], + fromNetwork: .appleSearchAds, + networkUserId: "attributionUser2") + + expect(self.backend.invokedPostAttributionDataCount) == 2 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + func testPostAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { - backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) attributionPoster.post(attributionData: ["something": "here"], fromNetwork: .adjust, networkUserId: "attributionUser1") - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 attributionPoster.post(attributionData: ["something": "else"], fromNetwork: .adjust, networkUserId: "attributionUser2") - expect(self.backend.invokedPostAdServicesTokenCount) == 0 + + expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } + @available(*, deprecated) + func testPostAppleSearchAdsAttributionIfNeededSkipsIfIAdFrameworkNotIncluded() { + MockAttributionTypeFactory.shouldReturnAdClientProxy = false + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 + } + +} + +#if os(iOS) +// `MockTrackingManagerProxy.mockAuthorizationStatus isn't available on tvOS +@available(iOS 14, *) +@available(*, deprecated) +class IOSAttributionPosterTests: BaseAttributionPosterTests { + + func testPostAppleSearchAdsAttributionIfNeededSkipsIfATTFrameworkNotIncludedOnNewOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = true + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = false + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + + func testPostAppleSearchAdsAttributionIfNeededPostsIfATTFrameworkNotIncludedOnOldOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = false + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = false + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + } + + func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnNewOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = true + + MockTrackingManagerProxy.mockAuthorizationStatus = .authorized + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + } + + func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnOldOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = false + MockTrackingManagerProxy.mockAuthorizationStatus = .authorized + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + } + + func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthNotDeterminedOnOldOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = false + MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + } + + func testPostAppleSearchAdsAttributionIfNeededSkipsIfAuthNotDeterminedOnNewOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = true + + MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 + } + + func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnOldOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = false + MockTrackingManagerProxy.mockAuthorizationStatus = .denied + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 + } + + func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnNewOS() throws { + systemInfo.stubbedIsOperatingSystemAtLeastVersion = true + MockTrackingManagerProxy.mockAuthorizationStatus = .denied + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 + } + + func testPostAppleSearchAdsAttributionIfNeededSkipsIfAlreadySent() throws { + MockTrackingManagerProxy.mockAuthorizationStatus = .authorized + MockAttributionTypeFactory.shouldReturnAdClientProxy = true + MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + + self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + + expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 + } +} +#endif + +#if canImport(AdServices) +@available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) +class AdServicesAttributionPosterTests: BaseAttributionPosterTests { + + func testPostAdServicesTokenIfNeededSkipsIfAlreadySent() { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 + + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 1 + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + + func testPostAdServicesTokenIfNeededSkipsIfNilToken() throws { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + + attributionFetcher.adServicesTokenToReturn = nil + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.backend.invokedPostAdServicesTokenCount) == 0 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + } + + func testPostAdServicesTokenIfNeededDoesNotCacheOnAPIError() throws { + let stubbedError: BackendError = .networkError( + .errorResponse(.init(code: .invalidAPIKey, message: nil), + 400) + ) + + backend.stubbedPostAdServicesTokenCompletionResult = .failure(stubbedError) + + attributionFetcher.adServicesTokenToReturn = nil + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 0 + } + } +#endif diff --git a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift index 66a0652e07..2179d6748c 100644 --- a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift +++ b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift @@ -21,4 +21,5 @@ class MockAttributionFetcher: AttributionFetcher { adServicesTokenCollectionCalled = true return adServicesTokenToReturn } + } diff --git a/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift b/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift index d2ade9cc59..0f6042e599 100644 --- a/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift +++ b/Tests/UnitTests/Mocks/MockAttributionTypeFactory.swift @@ -16,6 +16,25 @@ import Foundation #endif @testable import RevenueCat +class MockAdClientProxy: AfficheClientProxy { + + static var mockAttributionDetails: [String: NSObject] = [ + "Version3.1": + [ + "iad-campaign-id": 15292426, + "iad-attribution": true + ] as NSObject + ] + static var mockError: Error? + static var requestAttributionDetailsCallCount = 0 + + override func requestAttributionDetails(_ completionHandler: @escaping AttributionDetailsBlock) { + Self.requestAttributionDetailsCallCount += 1 + completionHandler(Self.mockAttributionDetails, Self.mockError) + } + +} + @available(iOS 14, macOS 11, tvOS 14, *) class MockTrackingManagerProxy: TrackingManagerProxy { @@ -29,6 +48,12 @@ class MockTrackingManagerProxy: TrackingManagerProxy { class MockAttributionTypeFactory: AttributionTypeFactory { + static var shouldReturnAdClientProxy = true + + override func afficheClientProxy() -> AfficheClientProxy? { + Self.shouldReturnAdClientProxy ? MockAdClientProxy() : nil + } + static var shouldReturnTrackingManagerProxy = true override func atFollowingProxy() -> TrackingManagerProxy? { diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index 5e89fd5096..f970d53663 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -92,6 +92,27 @@ class MockBackend: Backend { completion(self.stubbedGetCustomerInfoResult) } + var invokedPostAttributionData = false + var invokedPostAttributionDataCount = 0 + var invokedPostAttributionDataParameters: (data: [String: Any]?, network: AttributionNetwork, appUserID: String?)? + var invokedPostAttributionDataParametersList = [(data: [String: Any]?, + network: AttributionNetwork, + appUserID: String?)]() + var stubbedPostAttributionDataCompletionResult: (BackendError?, Void)? + + override func post(attributionData: [String: Any], + network: AttributionNetwork, + appUserID: String, + completion: ((BackendError?) -> Void)?) { + invokedPostAttributionData = true + invokedPostAttributionDataCount += 1 + invokedPostAttributionDataParameters = (attributionData, network, appUserID) + invokedPostAttributionDataParametersList.append((attributionData, network, appUserID)) + if let result = stubbedPostAttributionDataCompletionResult { + completion?(result.0) + } + } + var invokedPostAdServicesToken = false var invokedPostAdServicesTokenCount = 0 var invokedPostAdServicesTokenParameters: (token: String, appUserID: String?)? diff --git a/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift new file mode 100644 index 0000000000..90650eca50 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift @@ -0,0 +1,42 @@ +// +// 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 +// +// BackendPostAttributionDataTests.swift +// +// Created by Nacho Soto on 3/7/22. + +import Foundation +import Nimble +import XCTest + +@testable import RevenueCat + +class BackendPostAttributionDataTests: BaseBackendTests { + + override func createClient() -> MockHTTPClient { + super.createClient(#file) + } + + func testPostAttributesPutsDataInDataKey() throws { + self.httpClient.mock( + requestPath: .postAttributionData(appUserID: Self.userID), + response: .init(statusCode: .success) + ) + + let data: [String: AnyObject] = ["a": "b" as NSString, "c": "d" as NSString] + + backend.post(attributionData: data, + network: .adjust, + appUserID: Self.userID, + completion: nil) + + expect(self.httpClient.calls).toEventually(haveCount(1)) + } + +} diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json index 01705a4dc0..6843f2fb41 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS13-testPostAttributesPutsDataInDataKey.1.json @@ -8,7 +8,7 @@ "a" : "b", "c" : "d" }, - "network" : 0 + "network" : 1 }, "method" : "POST", "url" : "https://api.revenuecat.com/v1/subscribers/user/attribution" diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json index 01705a4dc0..6843f2fb41 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS14-testPostAttributesPutsDataInDataKey.1.json @@ -8,7 +8,7 @@ "a" : "b", "c" : "d" }, - "network" : 0 + "network" : 1 }, "method" : "POST", "url" : "https://api.revenuecat.com/v1/subscribers/user/attribution" diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json index 01705a4dc0..6843f2fb41 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS15-testPostAttributesPutsDataInDataKey.1.json @@ -8,7 +8,7 @@ "a" : "b", "c" : "d" }, - "network" : 0 + "network" : 1 }, "method" : "POST", "url" : "https://api.revenuecat.com/v1/subscribers/user/attribution" diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS16-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS16-testPostAttributesPutsDataInDataKey.1.json index 01705a4dc0..6843f2fb41 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS16-testPostAttributesPutsDataInDataKey.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS16-testPostAttributesPutsDataInDataKey.1.json @@ -8,7 +8,7 @@ "a" : "b", "c" : "d" }, - "network" : 0 + "network" : 1 }, "method" : "POST", "url" : "https://api.revenuecat.com/v1/subscribers/user/attribution" diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index f77b926fb7..7a8e888d16 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -127,12 +127,14 @@ class BasePurchasesTests: TestCase { var purchases: Purchases! func setupPurchases(automaticCollection: Bool = false) { + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = automaticCollection self.identityManager.mockIsAnonymous = false self.initializePurchasesInstance(appUserId: self.identityManager.currentAppUserID) } func setupAnonPurchases() { + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = false self.identityManager.mockIsAnonymous = true self.initializePurchasesInstance(appUserId: nil) } @@ -352,6 +354,32 @@ extension BasePurchasesTests { self.postedObserverMode = observerMode completion(self.postReceiptResult ?? .failure(.missingAppUserID())) } + + var invokedPostAttributionData = false + var invokedPostAttributionDataCount = 0 + // swiftlint:disable:next large_tuple + var invokedPostAttributionDataParameters: ( + data: [String: Any]?, + network: AttributionNetwork, + appUserID: String? + )? + var invokedPostAttributionDataParametersList = [(data: [String: Any]?, + network: AttributionNetwork, + appUserID: String?)]() + var stubbedPostAttributionDataCompletionResult: (BackendError?, Void)? + + override func post(attributionData: [String: Any], + network: AttributionNetwork, + appUserID: String, + completion: ((BackendError?) -> Void)? = nil) { + self.invokedPostAttributionData = true + self.invokedPostAttributionDataCount += 1 + self.invokedPostAttributionDataParameters = (attributionData, network, appUserID) + self.invokedPostAttributionDataParametersList.append((attributionData, network, appUserID)) + if let result = stubbedPostAttributionDataCompletionResult { + completion?(result.0) + } + } } } diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift index 167c9b0bb7..170ef41cc8 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift @@ -35,6 +35,24 @@ class PurchasesAttributionDataTests: BasePurchasesTests { expect(attributionData["rc_idfv"] as? String) == "rc_idfv" } + @available(*, deprecated) + func testPassesTheArrayForAllNetworks() { + self.setupPurchases() + + let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] + + Purchases.deprecated.addAttributionData(data, fromNetwork: .appleSearchAds) + + for key in data.keys { + expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains(key)) + .toEventually(beTrue()) + } + expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains("rc_idfa")) == true + expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains("rc_idfv")) == true + expect(self.backend.invokedPostAttributionDataParametersList[0].network) == .appleSearchAds + expect(self.backend.invokedPostAttributionDataParametersList[0].appUserID) == self.purchases?.appUserID + } + func testAttributionDataIsPostponedIfThereIsNoInstance() throws { let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] @@ -56,4 +74,77 @@ class PurchasesAttributionDataTests: BasePurchasesTests { expect(invokedParameters.appUserID) == self.purchases?.appUserID } + @available(*, deprecated) + func testAttributionDataSendsNetworkAppUserId() throws { + let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] + + Purchases.deprecated.addAttributionData(data, + from: .appleSearchAds, + forNetworkUserId: "newuser") + + self.setupPurchases() + + expect(self.backend.invokedPostAttributionData).toEventually(beTrue()) + + let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) + for key in data.keys { + expect(invokedMethodParams.data?.keys.contains(key)).to(beTrue()) + } + + expect(invokedMethodParams.data?.keys.contains("rc_idfa")) == true + expect(invokedMethodParams.data?.keys.contains("rc_idfv")) == true + expect(invokedMethodParams.data?.keys.contains("rc_attribution_network_id")) == true + expect(invokedMethodParams.data?["rc_attribution_network_id"] as? String) == "newuser" + expect(invokedMethodParams.network) == .appleSearchAds + expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID + } + + @available(*, deprecated) + func testAttributionDataDontSendNetworkAppUserIdIfNotProvided() throws { + let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] + + Purchases.deprecated.addAttributionData(data, fromNetwork: .appleSearchAds) + + self.setupPurchases() + + let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) + for key in data.keys { + expect(invokedMethodParams.data?.keys.contains(key)) == true + } + + expect(invokedMethodParams.data?.keys.contains("rc_idfa")) == true + expect(invokedMethodParams.data?.keys.contains("rc_idfv")) == true + expect(invokedMethodParams.data?.keys.contains("rc_attribution_network_id")) == false + expect(invokedMethodParams.network) == .appleSearchAds + expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID + } + + @available(*, deprecated) + func testAdClientAttributionDataIsAutomaticallyCollected() throws { + self.setupPurchases(automaticCollection: true) + + let invokedMethodParams = try XCTUnwrap(self.backend.invokedPostAttributionDataParameters) + + expect(invokedMethodParams).toNot(beNil()) + expect(invokedMethodParams.network) == .appleSearchAds + + let obtainedVersionData = try XCTUnwrap(invokedMethodParams.data?["Version3.1"] as? NSDictionary) + expect(obtainedVersionData["iad-campaign-id"]).toNot(beNil()) + } + + func testAdClientAttributionDataIsNotAutomaticallyCollectedIfDisabled() { + self.setupPurchases(automaticCollection: false) + expect(self.backend.invokedPostAttributionDataParameters).to(beNil()) + } + + func testAttributionDataPostponesMultiple() { + let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] + + Purchases.deprecated.addAttributionData(data, from: .adjust, forNetworkUserId: "newuser") + + self.setupPurchases(automaticCollection: true) + expect(self.backend.invokedPostAttributionDataParametersList.count) == 1 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParametersList.count) == 1 + } + } diff --git a/Tests/UnitTests/Purchasing/PurchasesDeprecation.swift b/Tests/UnitTests/Purchasing/PurchasesDeprecation.swift index f6abf15c6c..a06b266609 100644 --- a/Tests/UnitTests/Purchasing/PurchasesDeprecation.swift +++ b/Tests/UnitTests/Purchasing/PurchasesDeprecation.swift @@ -24,6 +24,8 @@ protocol PurchasesDeprecatable { from network: AttributionNetwork, forNetworkUserId networkUserId: String?) + static var automaticAppleSearchAdsAttributionCollection: Bool { get set } + } class PurchasesDeprecation: PurchasesDeprecatable { @@ -56,6 +58,12 @@ class PurchasesDeprecation: PurchasesDeprecatable { Purchases.addAttributionData(data, from: network, forNetworkUserId: networkUserId) } + @available(*, deprecated) + static var automaticAppleSearchAdsAttributionCollection: Bool { + get { return Purchases.automaticAppleSearchAdsAttributionCollection } + set { Purchases.automaticAppleSearchAdsAttributionCollection = newValue } + } + } extension Purchases { diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index d4072deae1..8c77145f26 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -134,7 +134,9 @@ class PurchasesSubscriberAttributesTests: TestCase { super.tearDown() } - func setupPurchases() { + func setupPurchases(automaticCollection: Bool = false) { + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = automaticCollection + self.mockIdentityManager.mockIsAnonymous = false let purchasesOrchestrator = PurchasesOrchestrator(productsManager: mockProductsManager, storeKitWrapper: mockStoreKitWrapper, From 4f99f74368ca5a34b3f9bab8eaceef8decd3ef2a Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Mon, 27 Jun 2022 18:32:10 -0700 Subject: [PATCH 07/16] use new mapKeys dictionary extension for device cache --- Sources/Caching/DeviceCache.swift | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index 6a2219f936..cbce50dca1 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -281,14 +281,14 @@ class DeviceCache { // convert keys from UserDefault from Integer String to AttributionNetwork let latestSent: [AttributionNetwork: String] = - latestAdvertisingIdsByRawNetworkSent.reduce(into: [:]) { adIdsByNetwork, adIdByRawNetworkString in - if let networkRawValue = Int(adIdByRawNetworkString.key), - let attributionNetwork = AttributionNetwork(rawValue: networkRawValue) { - adIdsByNetwork[attributionNetwork] = adIdByRawNetworkString.value - } else { - Logger.error(Strings.attribution.latest_attribution_sent_user_defaults_invalid) - } - } + latestAdvertisingIdsByRawNetworkSent.compactMapKeys { network in + guard let networkRawValue = Int(network), + let attributionNetwork = AttributionNetwork(rawValue: networkRawValue) else { + Logger.error(Strings.attribution.latest_attribution_sent_user_defaults_invalid) + return nil + } + return attributionNetwork + } return latestSent } @@ -297,10 +297,7 @@ class DeviceCache { func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { self.userDefaults.write { // convert AttributionNetwork to Integer as String - let latestAdIdsByRawNetworkStringSent = - latestAdvertisingIdsByNetworkSent.reduce(into: [:]) { adIdsByRawNetworkString, adIdByNetwork in - adIdsByRawNetworkString[String(adIdByNetwork.key.rawValue)] = adIdByNetwork.value - } + let latestAdIdsByRawNetworkStringSent = latestAdvertisingIdsByNetworkSent.mapKeys { $0.rawValue } $0.setValue(latestAdIdsByRawNetworkStringSent, forKey: CacheKeyBases.attributionDataDefaults + appUserID) } From 07efb5181afac9e6a2777f3a3ef5780a6d1dacd2 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Mon, 27 Jun 2022 18:48:37 -0700 Subject: [PATCH 08/16] update for some pr comments --- Sources/Attribution/AttributionPoster.swift | 8 ++++---- Sources/Caching/DeviceCache.swift | 12 +++++++----- Sources/Logging/Strings/AttributionStrings.swift | 6 +++--- Sources/Purchasing/Purchases/Attribution.swift | 16 ++++++---------- Tests/UnitTests/Mocks/MockDeviceCache.swift | 1 + 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Sources/Attribution/AttributionPoster.swift b/Sources/Attribution/AttributionPoster.swift index ad0c288610..7de97d7e9c 100644 --- a/Sources/Attribution/AttributionPoster.swift +++ b/Sources/Attribution/AttributionPoster.swift @@ -53,10 +53,10 @@ class AttributionPoster { } let currentAppUserID = self.currentUserProvider.currentAppUserID - guard let newDictToCache = getNewDictToCache(currentAppUserID: currentAppUserID, - idfa: identifierForAdvertisers, - network: network, - networkUserId: networkUserId) else { + guard let newDictToCache = self.getNewDictToCache(currentAppUserID: currentAppUserID, + idfa: identifierForAdvertisers, + network: network, + networkUserId: networkUserId) else { return } diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index cbce50dca1..b6f3ad254f 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -279,12 +279,15 @@ class DeviceCache { let key = CacheKeyBases.attributionDataDefaults + appUserID let latestAdvertisingIdsByRawNetworkSent = $0.object(forKey: key) as? [String: String] ?? [:] - // convert keys from UserDefault from Integer String to AttributionNetwork let latestSent: [AttributionNetwork: String] = - latestAdvertisingIdsByRawNetworkSent.compactMapKeys { network in - guard let networkRawValue = Int(network), + latestAdvertisingIdsByRawNetworkSent.compactMapKeys { networkKey in + guard let networkRawValue = Int(networkKey), let attributionNetwork = AttributionNetwork(rawValue: networkRawValue) else { - Logger.error(Strings.attribution.latest_attribution_sent_user_defaults_invalid) + Logger.error( + Strings.attribution.latest_attribution_sent_user_defaults_invalid( + networkKey: networkKey + ) + ) return nil } return attributionNetwork @@ -296,7 +299,6 @@ class DeviceCache { func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { self.userDefaults.write { - // convert AttributionNetwork to Integer as String let latestAdIdsByRawNetworkStringSent = latestAdvertisingIdsByNetworkSent.mapKeys { $0.rawValue } $0.setValue(latestAdIdsByRawNetworkStringSent, forKey: CacheKeyBases.attributionDataDefaults + appUserID) diff --git a/Sources/Logging/Strings/AttributionStrings.swift b/Sources/Logging/Strings/AttributionStrings.swift index fb075cf7e5..30e6a02870 100644 --- a/Sources/Logging/Strings/AttributionStrings.swift +++ b/Sources/Logging/Strings/AttributionStrings.swift @@ -42,7 +42,7 @@ enum AttributionStrings { case adservices_token_fetch_failed(error: Error) case adservices_token_post_failed(error: BackendError) case adservices_token_post_succeeded - case latest_attribution_sent_user_defaults_invalid + case latest_attribution_sent_user_defaults_invalid(networkKey: String) } @@ -132,8 +132,8 @@ extension AttributionStrings: CustomStringConvertible { case .adservices_token_post_succeeded: return "AdServices attribution token successfully posted" - case .latest_attribution_sent_user_defaults_invalid: - return "Attribution data stored in UserDefaults has invalid format." + case .latest_attribution_sent_user_defaults_invalid(let networkKey): + return "Attribution data stored in UserDefaults has invalid format for network key: \(networkKey)" } } diff --git a/Sources/Purchasing/Purchases/Attribution.swift b/Sources/Purchasing/Purchases/Attribution.swift index 09e7b79350..0242612337 100644 --- a/Sources/Purchasing/Purchases/Attribution.swift +++ b/Sources/Purchasing/Purchases/Attribution.swift @@ -47,28 +47,24 @@ import Foundation } +// should match OS availability in https://developer.apple.com/documentation/ad_services +@available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) public extension Attribution { /** * Enable automatic collection of AdServices attribution token. */ - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) @objc func enableAdServicesAttributionTokenCollection() { self.automaticAdServicesAttributionTokenCollection = true self.postAdServicesTokenIfNeeded() } - // should match OS availability in https://developer.apple.com/documentation/ad_services - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) internal func postAdServicesTokenIfNeeded() { - guard self.automaticAdServicesAttributionTokenCollection else { - return + if self.automaticAdServicesAttributionTokenCollection { + self.attributionPoster.postAdServicesTokenIfNeeded() } - self.attributionPoster.postAdServicesTokenIfNeeded() } } diff --git a/Tests/UnitTests/Mocks/MockDeviceCache.swift b/Tests/UnitTests/Mocks/MockDeviceCache.swift index a16f71b9d0..836310b53d 100644 --- a/Tests/UnitTests/Mocks/MockDeviceCache.swift +++ b/Tests/UnitTests/Mocks/MockDeviceCache.swift @@ -246,4 +246,5 @@ class MockDeviceCache: DeviceCache { override func latestAdvertisingIdsByNetworkSent(appUserID: String) -> [AttributionNetwork: String] { return invokedSetLatestNetworkAndAdvertisingIdsSentParameters?.adIdsByNetwork ?? [:] } + } From 4b48fa41fff17431257464c8b96b6088f3d2dca7 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Tue, 28 Jun 2022 09:31:43 -0700 Subject: [PATCH 09/16] remove purchasestests --- .../Purchasing/Purchases/PurchasesTests.swift | 855 ------------------ 1 file changed, 855 deletions(-) delete mode 100644 Tests/UnitTests/Purchasing/Purchases/PurchasesTests.swift diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesTests.swift deleted file mode 100644 index bedef168c9..0000000000 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesTests.swift +++ /dev/null @@ -1,855 +0,0 @@ -// -// Copyright RevenueCat Inc. All Rights Reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Created by RevenueCat. -// - -import Nimble -import StoreKit -import XCTest - -@testable import RevenueCat - -class PurchasesTests: BasePurchasesTests { - - func testDelegateIsCalledForRandomPurchaseSuccess() throws { - setupPurchases() - - let customerInfo = try CustomerInfo(data: Self.emptyCustomerInfoData) - self.backend.postReceiptResult = .success(customerInfo) - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let customerInfoBeforePurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [:] - ]]) - let customerInfoAfterPurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [product.mockProductIdentifier: []] - ]]) - self.backend.overrideCustomerInfoResult = .success(customerInfoBeforePurchase) - self.backend.postReceiptResult = .success(customerInfoAfterPurchase) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testDelegateIsOnlyCalledOnceIfCustomerInfoTheSame() throws { - setupPurchases() - - let customerInfo1 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0" - ] - ]) - - let customerInfo2 = customerInfo1 - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo1) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo2) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testDelegateIsCalledTwiceIfCustomerInfoTheDifferent() throws { - setupPurchases() - - let customerInfo1 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0" - ] - ]) - - let customerInfo2 = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "2.0" - ] - ]) - - let product = MockSK1Product(mockProductIdentifier: "product") - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo1) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(customerInfo2) - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(3)) - } - - func testIsAbleToFetchProducts() { - setupPurchases() - var products: [StoreProduct]? - let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.getProducts(productIdentifiers) { (newProducts) in - products = newProducts - } - - expect(products).toEventuallyNot(beNil()) - expect(products).toEventually(haveCount(productIdentifiers.count)) - } - - func testSetsSelfAsStoreKitWrapperDelegate() { - setupPurchases() - expect(self.storeKitWrapper.delegate).to(be(purchasesOrchestrator)) - } - - func testDoesntFetchProductDataIfEmptyList() { - setupPurchases() - var completionCalled = false - mockProductsManager.resetMock() - self.purchases.getProducts([]) { _ in - completionCalled = true - } - expect(completionCalled).toEventually(beTrue()) - expect(self.mockProductsManager.invokedProducts) == false - } - - func testSendsProductDataIfProductIsCached() throws { - setupPurchases() - let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.getProducts(productIdentifiers) { (newProducts) in - let product = newProducts[0] - self.purchases.purchase(product: newProducts[0]) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(CustomerInfo(testData: Self.emptyCustomerInfoData)!) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedReceiptData).toNot(beNil()) - - expect(self.backend.postedProductID).to(equal(product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(product.price as Decimal)) - - if #available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) { - expect(self.backend.postedPaymentMode).to(equal(StoreProductDiscount.PaymentMode.payAsYouGo)) - expect(self.backend.postedIntroPrice).to(equal(product.introductoryDiscount?.price)) - } else { - expect(self.backend.postedPaymentMode).to(beNil()) - expect(self.backend.postedIntroPrice).to(beNil()) - } - - if #available(iOS 12.0, tvOS 12.0, macOS 10.14, *) { - expect(self.backend.postedSubscriptionGroup).to(equal(product.subscriptionGroupIdentifier)) - } - - if #available(iOS 12.2, *) { - expect(self.backend.postedDiscounts?.count).to(equal(1)) - let postedDiscount: StoreProductDiscount = self.backend.postedDiscounts![0] - expect(postedDiscount.offerIdentifier).to(equal("discount_id")) - expect(postedDiscount.price).to(equal(1.99)) - let expectedPaymentMode = StoreProductDiscount.PaymentMode.payAsYouGo.rawValue - expect(postedDiscount.paymentMode.rawValue).to(equal(expectedPaymentMode)) - } - - expect(self.backend.postedCurrencyCode) == product.priceFormatter!.currencyCode - - expect(self.storeKitWrapper.finishCalled).toEventually(beTrue()) - } - } - - func testFetchesProductDataIfNotCached() throws { - systemInfo.stubbedIsApplicationBackgrounded = true - setupPurchases() - let sk1Product = MockSK1Product(mockProductIdentifier: "com.product.id1") - let product = StoreProduct(sk1Product: sk1Product) - - let transaction = MockTransaction() - storeKitWrapper.payment = SKPayment(product: sk1Product) - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchasing - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: Self.emptyCustomerInfoData)) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.mockProductsManager.invokedProductsParameters).toEventually(contain([product.productIdentifier])) - - expect(self.backend.postedProductID).toNot(beNil()) - expect(self.backend.postedPrice).toNot(beNil()) - expect(self.backend.postedCurrencyCode).toNot(beNil()) - if #available(iOS 12.2, macOS 10.14.4, *) { - expect(self.backend.postedIntroPrice).toNot(beNil()) - } - } - - func testDoesntIgnorePurchasesThatDoNotHaveApplicationUserNames() { - setupPurchases() - let transaction = MockTransaction() - - let payment = SKMutablePayment() - payment.productIdentifier = "test" - - expect(payment.applicationUsername).to(beNil()) - - transaction.mockPayment = payment - transaction.mockState = SKPaymentTransactionState.purchased - - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testDoesntSetWrapperDelegateToNilIfDelegateNil() { - setupPurchases() - purchases!.delegate = nil - - expect(self.storeKitWrapper.delegate).toNot(beNil()) - - purchases!.delegate = purchasesDelegate - - expect(self.storeKitWrapper.delegate).toNot(beNil()) - } - - func testSubscribesToUIApplicationDidBecomeActive() { - setupPurchases() - expect(self.notificationCenter.observers.count).to(equal(2)) - if self.notificationCenter.observers.count > 0 { - let (_, _, name, _) = self.notificationCenter.observers[0] - expect(name).to(equal(SystemInfo.applicationDidBecomeActiveNotification)) - } - } - - func testTriggersCallToBackend() { - setupPurchases() - notificationCenter.fireNotifications() - expect(self.backend.userID).toEventuallyNot(beNil()) - } - - func testAutomaticallyFetchesCustomerInfoOnDidBecomeActiveIfCacheStale() { - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - - self.deviceCache.stubbedIsCustomerInfoCacheStale = true - notificationCenter.fireNotifications() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(2)) - } - - func testDoesntAutomaticallyFetchCustomerInfoOnDidBecomeActiveIfCacheValid() { - setupPurchases() - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - self.deviceCache.stubbedIsCustomerInfoCacheStale = false - - notificationCenter.fireNotifications() - - expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) - } - - func testAutomaticallyCallsDelegateOnDidBecomeActiveAndUpdate() { - setupPurchases() - notificationCenter.fireNotifications() - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(1)) - } - - func testDoesntRemoveObservationWhenDelegateNil() { - setupPurchases() - purchases!.delegate = nil - - expect(self.notificationCenter.observers.count).to(equal(2)) - } - - func testSyncPurchasesPostsTheReceipt() { - setupPurchases() - purchases.syncPurchases(completion: nil) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testSyncPurchasesPostsTheReceiptIfAutoSyncPurchasesSettingIsOff() throws { - systemInfo = try MockSystemInfo(platformInfo: nil, - finishTransactions: false, - dangerousSettings: DangerousSettings(autoSyncPurchases: false)) - self.initializePurchasesInstance(appUserId: nil) - - purchases.syncPurchases(completion: nil) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - func testSyncPurchasesDoesntPostIfReceiptEmptyAndCustomerInfoLoaded() throws { - let info = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let object = try info.asData() - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == false - } - - func testSyncPurchasesPostsIfReceiptEmptyAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = false - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoLoaded() throws { - let info = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:], - "original_application_version": "1.0", - "original_purchase_date": "2018-10-26T23:17:53Z" - ]]) - - let object = try info.asData() - self.deviceCache.cachedCustomerInfo[identityManager.currentAppUserID] = object - - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesPostsIfReceiptHasTransactionsAndCustomerInfoNotLoaded() { - mockTransactionsManager.stubbedCustomerHasTransactionsCompletionParameter = true - - setupPurchases() - purchases.syncPurchases(completion: nil) - - expect(self.backend.postReceiptDataCalled) == true - } - - func testSyncPurchasesDoesntRefreshTheReceiptIfNotEmpty() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = true - purchases.syncPurchases(completion: nil) - - expect(self.receiptFetcher.receiptDataTimesCalled) == 1 - expect(self.requestFetcher.refreshReceiptCalled) == false - } - - func testSyncPurchasesDoesntRefreshTheReceiptIfEmpty() { - setupPurchases() - self.receiptFetcher.shouldReturnReceipt = false - purchases.syncPurchases(completion: nil) - - expect(self.receiptFetcher.receiptDataTimesCalled) == 1 - expect(self.requestFetcher.refreshReceiptCalled) == false - } - - func testSyncPurchasesPassesIsRestoreAsAllowSharingAppStoreAccount() { - setupPurchases() - - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = false - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == false - - deprecated.allowSharingAppStoreAccount = true - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == true - } - - func testSyncPurchasesSetsIsRestoreForAnon() { - setupAnonPurchases() - - var deprecated = purchases.deprecated - deprecated.allowSharingAppStoreAccount = false - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == false - - deprecated.allowSharingAppStoreAccount = true - purchases.syncPurchases(completion: nil) - expect(self.backend.postedIsRestore!) == true - } - - func testSyncPurchasesCallsSuccessDelegateMethod() throws { - setupPurchases() - - let customerInfo = try CustomerInfo(data: Self.emptyCustomerInfoData) - self.backend.postReceiptResult = .success(customerInfo) - - var receivedCustomerInfo: CustomerInfo? - - purchases!.syncPurchases { (info, _) in - receivedCustomerInfo = info - } - - expect(receivedCustomerInfo).toEventually(be(customerInfo)) - } - - func testSyncPurchasesPassesErrorOnFailure() { - setupPurchases() - - let error: BackendError = .missingAppUserID() - - self.backend.postReceiptResult = .failure(error) - self.purchasesDelegate.customerInfo = nil - - var receivedError: Error? - - purchases!.syncPurchases { (_, newError) in - receivedError = newError - } - - expect(receivedError).toEventuallyNot(beNil()) - expect(receivedError).to(matchError(error.asPurchasesError)) - } - - func testCallsShouldAddPromoPaymentDelegateMethod() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "mock_product")) - let payment = SKPayment() - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product.sk1Product!) - - expect(self.purchasesDelegate.promoProduct) == product - } - - func testShouldAddPromoPaymentDelegateMethodReturnsFalse() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment() - - let result = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(result).to(beFalse()) - } - - func testPromoPaymentDelegateMethodMakesRightCalls() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - let transaction = MockTransaction() - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.backend.postedProductID).to(equal(product.productIdentifier)) - expect(self.backend.postedPrice).to(equal(product.price as Decimal)) - } - - func testPromoPaymentDelegateMethodCachesProduct() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - let transaction = MockTransaction() - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.mockProductsManager.invokedCacheProduct) == true - expect(self.mockProductsManager.invokedCacheProductParameter) == product - } - - func testDeferBlockMakesPayment() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - guard let storeKitWrapperDelegate = storeKitWrapper.delegate else { - fail("storeKitWrapperDelegate nil") - return - } - - _ = storeKitWrapperDelegate.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(self.purchasesDelegate.makeDeferredPurchase).toNot(beNil()) - - expect(self.storeKitWrapper.payment).to(beNil()) - - guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { - fail("makeDeferredPurchase should have been nonNil") - return - } - - makeDeferredPurchase { (_, _, _, _) in - } - - expect(self.storeKitWrapper.payment).to(be(payment)) - } - - func testGetEligibility() { - setupPurchases() - purchases.checkTrialOrIntroDiscountEligibility(productIdentifiers: []) { (_) in - } - - expect(self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore) - .to(beTrue()) - } - - func testCachesCustomerInfo() { - setupPurchases() - - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(1)) - expect(self.deviceCache.cachedCustomerInfo[self.purchases!.appUserID]).toEventuallyNot(beNil()) - - let customerInfo = self.deviceCache.cachedCustomerInfo[self.purchases!.appUserID] - - do { - if customerInfo != nil { - try JSONSerialization.jsonObject(with: customerInfo!, options: []) - } - } catch { - fail() - } - } - - func testCachesCustomerInfoOnPurchase() throws { - setupPurchases() - - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(1)) - - self.backend.postReceiptResult = .success(try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "other_purchases": [:] - ]])) - - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in - - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - - expect(self.deviceCache.cacheCustomerInfoCount).toEventually(equal(2)) - } - - func testAddAttributionAlwaysAddsAdIdsEmptyDict() { - setupPurchases() - - Purchases.deprecated.addAttributionData([:], fromNetwork: AttributionNetwork.adjust) - - // swiftlint:disable:next line_length - let attributionData = self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParameters?.attributionData - expect(attributionData?.count) == 2 - expect(attributionData?["rc_idfa"] as? String) == "rc_idfa" - expect(attributionData?["rc_idfv"] as? String) == "rc_idfv" - } - - func testWhenNoReceiptDataReceiptIsRefreshed() { - setupPurchases() - receiptFetcher.shouldReturnReceipt = true - receiptFetcher.shouldReturnZeroBytesReceipt = true - - self.makeAPurchase() - - expect(self.receiptFetcher.receiptDataCalled) == true - expect(self.receiptFetcher.receiptDataReceivedRefreshPolicy) == .onlyIfEmpty - } - - func testPaymentSheetCancelledErrorIsParsedCorrectly() { - setupPurchases() - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - var receivedUserCancelled: Bool? - var receivedError: NSError? - - let unknownError = NSError( - domain: SKErrorDomain, - code: 907, - userInfo: [ - NSUnderlyingErrorKey: NSError( - domain: "AMSErrorDomain", - code: 6, - userInfo: [:] - ) - ] - ) - - self.purchases.purchase(product: product) { (_, _, error, userCancelled) in - receivedError = error as NSError? - receivedUserCancelled = userCancelled - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = .failed - transaction.mockError = unknownError - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(receivedUserCancelled).toEventuallyNot(beNil()) - expect(receivedUserCancelled) == true - expect(receivedError).to(matchError(ErrorCode.purchaseCancelledError)) - } - - func testDeferBlockCallsCompletionBlockAfterPurchaseCompletes() { - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "mock_product") - let payment = SKPayment.init(product: product) - - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) - - expect(self.purchasesDelegate.makeDeferredPurchase).toNot(beNil()) - - expect(self.storeKitWrapper.payment).to(beNil()) - - var completionCalled = false - - guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { - fail("makeDeferredPurchase nil") - return - } - - makeDeferredPurchase { (_, _, _, _) in - completionCalled = true - } - - let transaction = MockTransaction() - transaction.mockPayment = self.storeKitWrapper.payment! - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.storeKitWrapper.payment).to(be(payment)) - expect(completionCalled).toEventually(beTrue()) - } - - func testAttributionDataIsPostponedIfThereIsNoInstance() { - let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - - Purchases.deprecated.addAttributionData(data, fromNetwork: AttributionNetwork.appsFlyer) - - setupPurchases() - - let invokedParameters = self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParameters - expect(invokedParameters?.attributionData).toNot(beNil()) - - for key in data.keys { - expect(invokedParameters?.attributionData.keys.contains(key)).toEventually(beTrue()) - } - - expect(invokedParameters?.attributionData.keys.contains("rc_idfa")) == true - expect(invokedParameters?.attributionData.keys.contains("rc_idfv")) == true - expect(invokedParameters?.network) == AttributionNetwork.appsFlyer - expect(invokedParameters?.appUserID) == self.purchases?.appUserID - } - - func testIsAnonymous() { - setupAnonPurchases() - expect(self.purchases.isAnonymous).to(beTrue()) - } - - func testIsNotAnonymous() { - setupPurchases() - expect(self.purchases.isAnonymous).to(beFalse()) - } - - func testProductIsRemovedButPresentInTheQueuedTransaction() throws { - self.mockProductsManager.stubbedProductsCompletionResult = Set() - setupPurchases() - let product = MockSK1Product(mockProductIdentifier: "product") - - let customerInfoBeforePurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [:] - ]]) - let customerInfoAfterPurchase = try CustomerInfo(data: [ - "request_date": "2019-08-16T10:30:42Z", - "subscriber": [ - "first_seen": "2019-07-17T00:05:54Z", - "original_app_user_id": "app_user_id", - "subscriptions": [:], - "non_subscriptions": [product.mockProductIdentifier: []] - ]]) - self.backend.overrideCustomerInfoResult = .success(customerInfoBeforePurchase) - self.backend.postReceiptResult = .success(customerInfoAfterPurchase) - - let payment = SKPayment(product: product) - - let transaction = MockTransaction() - - transaction.mockPayment = payment - - transaction.mockState = SKPaymentTransactionState.purchasing - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - transaction.mockState = SKPaymentTransactionState.purchased - self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - - expect(self.backend.postReceiptDataCalled).to(beTrue()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(2)) - } - - func testProxyURL() { - expect(SystemInfo.proxyURL).to(beNil()) - let defaultHostURL = URL(string: "https://api.revenuecat.com") - expect(SystemInfo.serverHostURL) == defaultHostURL - - let testURL = URL(string: "https://test_url") - Purchases.proxyURL = testURL - - expect(SystemInfo.serverHostURL) == testURL - - Purchases.proxyURL = nil - - expect(SystemInfo.serverHostURL) == defaultHostURL - } - - @available(iOS 14.0, macOS 14.0, tvOS 14.0, watchOS 7.0, *) - func testSyncsPurchasesIfEntitlementsRevokedForProductIDs() throws { - try AvailabilityChecks.iOS14APIAvailableOrSkipTest() - - setupPurchases() - guard purchases != nil else { fatalError() } - expect(self.backend.postReceiptDataCalled).to(beFalse()) - (purchasesOrchestrator as StoreKitWrapperDelegate) - .storeKitWrapper(storeKitWrapper, didRevokeEntitlementsForProductIdentifiers: ["a", "b"]) - expect(self.backend.postReceiptDataCalled).to(beTrue()) - } - - @available(*, deprecated) // Ignore deprecation warnings - func testSetDebugLogsEnabledSetsTheCorrectValue() { - Logger.logLevel = .warn - - Purchases.debugLogsEnabled = true - expect(Logger.logLevel) == .debug - - Purchases.debugLogsEnabled = false - expect(Logger.logLevel) == .info - } - - private func verifyUpdatedCaches(newAppUserID: String) { - let expectedCallCount = 2 - expect(self.backend.getSubscriberCallCount).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.cachedCustomerInfo.count).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.cachedCustomerInfo[newAppUserID]).toEventuallyNot(beNil()) - expect(self.purchasesDelegate.customerInfoReceivedCount).toEventually(equal(expectedCallCount)) - expect(self.deviceCache.setCustomerInfoCacheTimestampToNowCount).toEventually(equal(expectedCallCount)) - expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(expectedCallCount)) - } - -} From 30135d613952a0fa1d619bae6de1ce24ec1bdbb1 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Tue, 28 Jun 2022 16:09:59 -0700 Subject: [PATCH 10/16] generate missing snapshots --- ...OS12-testPostAdServicesCallsHttpClient.1.json | 12 ++++++++++++ ...OS13-testPostAdServicesCallsHttpClient.1.json | 12 ++++++++++++ ...OS16-testPostAdServicesCallsHttpClient.1.json | 12 ++++++++++++ ...12-testPostAttributesPutsDataInDataKey.1.json | 16 ++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS12-testPostAdServicesCallsHttpClient.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS13-testPostAdServicesCallsHttpClient.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS16-testPostAdServicesCallsHttpClient.1.json create mode 100644 Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS12-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS12-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..7a9dd746e4 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS12-testPostAdServicesCallsHttpClient.1.json @@ -0,0 +1,12 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "aad_attribution_token" : "asdf" + }, + "method" : "POST", + "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/asdf\/adservices_attribution" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS13-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS13-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..d3a2b02590 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS13-testPostAdServicesCallsHttpClient.1.json @@ -0,0 +1,12 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "aad_attribution_token" : "asdf" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/subscribers/asdf/adservices_attribution" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS16-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS16-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..d3a2b02590 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS16-testPostAdServicesCallsHttpClient.1.json @@ -0,0 +1,12 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "aad_attribution_token" : "asdf" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/subscribers/asdf/adservices_attribution" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json new file mode 100644 index 0000000000..a6115f95fb --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json @@ -0,0 +1,16 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret" + }, + "request" : { + "body" : { + "data" : { + "a" : "b", + "c" : "d" + }, + "network" : 1 + }, + "method" : "POST", + "url" : "https:\/\/api.revenuecat.com\/v1\/subscribers\/user\/attribution" + } +} \ No newline at end of file From a511a742bb87cc905067265c9da727e9397acc24 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Tue, 28 Jun 2022 16:10:22 -0700 Subject: [PATCH 11/16] fix deprecation messages to clarify method now called on shared instance --- Sources/Misc/Deprecations.swift | 2 +- Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index 4bd639edc3..e9d0ff23d2 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -146,7 +146,7 @@ public extension Purchases { /** * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. */ - @available(*, deprecated, message: "Use Purchases.attribution.enableAdServicesAttributionTokenCollection() instead") + @available(*, deprecated, message: "Use Purchases.shared.attribution.enableAdServicesAttributionTokenCollection() instead") @objc static var automaticAppleSearchAdsAttributionCollection: Bool = false } diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 74e7ed564f..8d6ad5292d 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -76,7 +76,8 @@ + (void)checkAPI { [RCPurchases addAttributionData:@{} fromNetwork:RCAttributionNetworkBranch forNetworkUserId:nil]; // should have deprecation warning: - // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use Purchases.attribution.enableAdServicesAttributionTokenCollection instead + // 'automaticAppleSearchAdsAttributionCollection' is deprecated: Use + // Purchases.shared.attribution.enableAdServicesAttributionTokenCollection instead automaticAppleSearchAdsAttributionCollection = [RCPurchases automaticAppleSearchAdsAttributionCollection]; // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead From 3cee7b596546260c7519defba0fcabf71e961f91 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Wed, 29 Jun 2022 12:25:58 -0700 Subject: [PATCH 12/16] Fix for inserting nonstring into userdefaults --- Sources/Caching/DeviceCache.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index b6f3ad254f..6e4a08940e 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -299,7 +299,7 @@ class DeviceCache { func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { self.userDefaults.write { - let latestAdIdsByRawNetworkStringSent = latestAdvertisingIdsByNetworkSent.mapKeys { $0.rawValue } + let latestAdIdsByRawNetworkStringSent = latestAdvertisingIdsByNetworkSent.mapKeys { String($0.rawValue) } $0.setValue(latestAdIdsByRawNetworkStringSent, forKey: CacheKeyBases.attributionDataDefaults + appUserID) } From 760a4c19002b532e527e96a8cec6e45e8f04e1ac Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Wed, 29 Jun 2022 15:20:49 -0700 Subject: [PATCH 13/16] Add tests for bug --- .../Attribution/AttributionPosterTests.swift | 11 +++++++++ .../UnitTests/Caching/DeviceCacheTests.swift | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index 725439e69b..9bbdff7c92 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -329,5 +329,16 @@ class AdServicesAttributionPosterTests: BaseAttributionPosterTests { expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 0 } + func testPostAdServicesTokenCachesProperData() throws { + backend.stubbedPostAdServicesTokenCompletionResult = .success(()) + + let adServicesToken = "asdf" + attributionFetcher.adServicesTokenToReturn = adServicesToken + attributionPoster.postAdServicesTokenIfNeeded() + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 + expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentParameters) == + ([.adServices:adServicesToken], currentUserProvider.currentAppUserID) + } + } #endif diff --git a/Tests/UnitTests/Caching/DeviceCacheTests.swift b/Tests/UnitTests/Caching/DeviceCacheTests.swift index 1c67a36315..5c376c7bc4 100644 --- a/Tests/UnitTests/Caching/DeviceCacheTests.swift +++ b/Tests/UnitTests/Caching/DeviceCacheTests.swift @@ -404,4 +404,27 @@ class DeviceCacheTests: TestCase { expect(mockCachedObject.invokedClearCache) == true } + func testSetLatestAdvertisingIdsByNetworkSentMapsAttributionNetworksToStringKeys() { + let userId = "asdf" + let token = "token" + let latestAdIdsByNetworkSent = [AttributionNetwork.adServices: token] + self.deviceCache.set(latestAdvertisingIdsByNetworkSent: latestAdIdsByNetworkSent, appUserID: userId) + + let key = "com.revenuecat.userdefaults.attribution." + userId + expect(self.mockUserDefaults.object(forKey: key) as? [String: String] ?? [:]) == + [String(AttributionNetwork.adServices.rawValue): token] + } + + func testSetLatestAdvertisingIdsByNetworkSentMapsStringKeysToAttributionNetworks() { + let userId = "asdf" + let token = "token" + let key = "com.revenuecat.userdefaults.attribution." + userId + let cachedValue = [String(AttributionNetwork.adServices.rawValue): token] + + self.mockUserDefaults.mockValues = [key: cachedValue] + + expect(self.deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: userId)) == + [AttributionNetwork.adServices: token] + } + } From cdb8df612e0b57c013eef6870ae832439e7a5611 Mon Sep 17 00:00:00 2001 From: Maddie Beyl Date: Thu, 30 Jun 2022 13:16:48 -0700 Subject: [PATCH 14/16] fix lint --- Tests/UnitTests/Attribution/AttributionPosterTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index 9bbdff7c92..e4c22b9083 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -337,7 +337,7 @@ class AdServicesAttributionPosterTests: BaseAttributionPosterTests { attributionPoster.postAdServicesTokenIfNeeded() expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentCount) == 1 expect(self.deviceCache.invokedSetLatestNetworkAndAdvertisingIdsSentParameters) == - ([.adServices:adServicesToken], currentUserProvider.currentAppUserID) + ([.adServices: adServicesToken], currentUserProvider.currentAppUserID) } } From d33d50120ea51d3ea9c617b2be68fb26ce245d50 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Tue, 2 Aug 2022 15:47:20 -0500 Subject: [PATCH 15/16] Added availablility checks for tests and removed unused varialbes --- Sources/Attribution/AttributionPoster.swift | 3 +-- Tests/UnitTests/Attribution/AttributionPosterTests.swift | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Attribution/AttributionPoster.swift b/Sources/Attribution/AttributionPoster.swift index 7de97d7e9c..5a7b1a346b 100644 --- a/Sources/Attribution/AttributionPoster.swift +++ b/Sources/Attribution/AttributionPoster.swift @@ -128,8 +128,7 @@ class AttributionPoster { @available(tvOS, unavailable) @available(watchOS, unavailable) func postAdServicesTokenIfNeeded() { - let latestTokenSent = latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) - guard latestTokenSent == nil else { + guard latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) == nil else { return } diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index e4c22b9083..5cdcf40d8a 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -79,6 +79,11 @@ class BaseAttributionPosterTests: TestCase { class AttributionPosterTests: BaseAttributionPosterTests { + override func setUpWithError() throws { + try super.setUpWithError() + try AvailabilityChecks.iOS14APIAvailableOrSkipTest() + } + func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" backend.stubbedPostAttributionDataCompletionResult = (nil, ()) From 04363d7483bec2845b674548ff48b89c60b46c60 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Tue, 2 Aug 2022 15:49:52 -0500 Subject: [PATCH 16/16] Whoops... wrong class --- .../UnitTests/Attribution/AttributionPosterTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/UnitTests/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index 5cdcf40d8a..25c5936b67 100644 --- a/Tests/UnitTests/Attribution/AttributionPosterTests.swift +++ b/Tests/UnitTests/Attribution/AttributionPosterTests.swift @@ -79,11 +79,6 @@ class BaseAttributionPosterTests: TestCase { class AttributionPosterTests: BaseAttributionPosterTests { - override func setUpWithError() throws { - try super.setUpWithError() - try AvailabilityChecks.iOS14APIAvailableOrSkipTest() - } - func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" backend.stubbedPostAttributionDataCompletionResult = (nil, ()) @@ -298,6 +293,11 @@ class IOSAttributionPosterTests: BaseAttributionPosterTests { @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) class AdServicesAttributionPosterTests: BaseAttributionPosterTests { + override func setUpWithError() throws { + try super.setUpWithError() + try AvailabilityChecks.iOS14APIAvailableOrSkipTest() + } + func testPostAdServicesTokenIfNeededSkipsIfAlreadySent() { backend.stubbedPostAdServicesTokenCompletionResult = .success(())