diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 415f2c2c88..e670f35782 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 */ @@ -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 */; }; @@ -279,7 +278,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 */; }; @@ -344,13 +342,20 @@ 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 */; }; + 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 */; }; B300E4BF26D436F900B22262 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; }; B300E4C026D4371200B22262 /* SKPaymentTransactionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492526B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift */; }; @@ -379,7 +384,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 /* CustomerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34605D0279A6E600031CA74 /* CustomerAPI.swift */; }; @@ -447,7 +451,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 */; }; @@ -670,7 +673,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 = ""; }; @@ -770,7 +772,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 = ""; }; @@ -829,12 +830,19 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -857,7 +865,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 /* CustomerAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerAPI.swift; sourceTree = ""; }; @@ -924,7 +931,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 = ""; }; @@ -952,6 +958,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A5B6CDD8280F3843007629D5 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1455,6 +1462,7 @@ 3530C18722653E8F00D6DF52 /* Frameworks */ = { isa = PBXGroup; children = ( + A5B6CDD5280F3843007629D5 /* AdServices.framework */, B36824BD268FBC5B00957E4C /* XCTest.framework */, 2DE20B9126409ECF004C597D /* StoreKit.framework */, 2DE20B7526408806004C597D /* StoreKitTest.framework */, @@ -1721,15 +1729,16 @@ isa = PBXGroup; children = ( 5796A39727D6C07D00653165 /* __Snapshots__ */, + A56C2E002819C33500995421 /* BackendPostAdServicesTokenTests.swift */, 5796A38927D6B96300653165 /* BackendGetCustomerInfoTests.swift */, 5796A38F27D6BCD100653165 /* BackendGetIntroEligibilityTests.swift */, 5796A39327D6BD6900653165 /* BackendGetOfferingsTests.swift */, 5796A38B27D6BA1600653165 /* BackendLoginTests.swift */, - 5796A39A27D6C20A00653165 /* BackendPostAttributionDataTests.swift */, 5796A39527D6BDAB00653165 /* BackendPostOfferForSigningTests.swift */, 5796A39827D6C1E000653165 /* BackendPostSubscriberAttributesTests.swift */, 5796A38727D6B85900653165 /* BackendPostReceiptDataTests.swift */, 5796A38027D6B78500653165 /* BaseBackendTest.swift */, + A56DFDF1286665E600EF2E32 /* BackendPostAttributionDataTests.swift */, ); path = Backend; sourceTree = ""; @@ -1796,10 +1805,11 @@ B34605BA279A6E380031CA74 /* GetOfferingsOperation.swift */, B34605B7279A6E380031CA74 /* LogInOperation.swift */, B34605AB279A6E380031CA74 /* NetworkOperation.swift */, - B34605B8279A6E380031CA74 /* PostAttributionDataOperation.swift */, B34605AD279A6E380031CA74 /* PostOfferForSigningOperation.swift */, B34605B6279A6E380031CA74 /* PostReceiptDataOperation.swift */, B34605B9279A6E380031CA74 /* PostSubscriberAttributesOperation.swift */, + A55D5D65282ECCC100FA7623 /* PostAdServicesTokenOperation.swift */, + A56DFDEF286643BF00EF2E32 /* PostAttributionDataOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1895,7 +1905,6 @@ F5BE44412698580200254A30 /* Attribution */ = { isa = PBXGroup; children = ( - F5BE4478269E4A4C00254A30 /* AfficheClientProxy.swift */, F5BE447C269E4ADB00254A30 /* ASIdManagerProxy.swift */, 37E35CD16BB73BB091E64D9A /* AttributionData.swift */, 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */, @@ -1903,6 +1912,7 @@ B3A55B7C26C452A7007EFC56 /* AttributionPoster.swift */, F5BE44422698581100254A30 /* AttributionTypeFactory.swift */, F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */, + A56DFDEB2866438B00EF2E32 /* AfficheClientProxy.swift */, ); path = Attribution; sourceTree = ""; @@ -1910,8 +1920,8 @@ F5BE444626985E6E00254A30 /* Attribution */ = { isa = PBXGroup; children = ( - 37E354FE32DD3EA3FF3ECD0A /* AttributionPosterTests.swift */, 37E350420D54B99BB39448E0 /* AttributionTypeFactoryTests.swift */, + A524378A284FFF0200E788BD /* AttributionPosterTests.swift */, ); path = Attribution; sourceTree = ""; @@ -2317,6 +2327,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 */, @@ -2364,7 +2375,6 @@ A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */, B378156D285A9772000A7B93 /* OfferingsAPI.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 */, @@ -2423,6 +2433,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 */, @@ -2456,6 +2467,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 */, @@ -2477,7 +2489,6 @@ 37E3578711F5FDD5DC6458A8 /* AttributionFetcher.swift in Sources */, F5714EA526D6C24D00635477 /* JSONDecoder+Extensions.swift in Sources */, B302206A27271BCB008F1A0D /* Decoder+Extensions.swift in Sources */, - B34605CD279A6E380031CA74 /* PostAttributionDataOperation.swift in Sources */, B34605C1279A6E380031CA74 /* NetworkOperation.swift in Sources */, 5766AA56283D4C5400FA6091 /* IgnoreHashable.swift in Sources */, 5766C622282DAA700067D886 /* GetIntroEligibilityResponse.swift in Sources */, @@ -2517,7 +2528,6 @@ 57E415FF28469EAB00EA5460 /* PurchasesGetProductsTests.swift in Sources */, 351B514726D44A0D00BD2BD7 /* MockSystemInfo.swift in Sources */, B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */, - 5796A39B27D6C20A00653165 /* BackendPostAttributionDataTests.swift in Sources */, 57554C62282ABFD9009A7E58 /* StoreTests.swift in Sources */, 2DDF41CA24F6F4C3005BC22D /* ArraySlice_UInt8+ExtensionsTests.swift in Sources */, 2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */, @@ -2531,6 +2541,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 */, @@ -2544,7 +2555,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 */, @@ -2561,8 +2571,10 @@ 351B517226D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift in Sources */, 57D56FCA2853C005009E8E1E /* StringExtensionsTests.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/AttributionFetcher.swift b/Sources/Attribution/AttributionFetcher.swift index 1d02c4cabf..4d58f19342 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 } @@ -82,6 +88,23 @@ class AttributionFetcher { #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? { +#if canImport(AdServices) + do { + return try AAAttribution.attributionToken() + } catch { + let message = Strings.attribution.adservices_token_fetch_failed(error: error) + Logger.appleWarning(message) + return nil + } +#else + 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/ diff --git a/Sources/Attribution/AttributionNetwork.swift b/Sources/Attribution/AttributionNetwork.swift index 04ccd0d52e..52a9af04dd 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 } @@ -64,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 40df8469b5..5a7b1a346b 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?) { @@ -54,19 +53,13 @@ class AttributionPoster { } let currentAppUserID = self.currentUserProvider.currentAppUserID - let networkKey = String(network.rawValue) - let latestNetworkIdsAndAdvertisingIdsSentByNetwork = - deviceCache.latestNetworkAndAdvertisingIdsSent(appUserID: currentAppUserID) - let latestSentToNetwork = latestNetworkIdsAndAdvertisingIdsSentByNetwork[networkKey] - - let newValueForNetwork = "\(identifierForAdvertisers ?? "(null)")_\(networkUserId ?? "(null)")" - guard latestSentToNetwork != newValueForNetwork else { - Logger.debug(Strings.attribution.skip_same_attributes) + guard let newDictToCache = self.getNewDictToCache(currentAppUserID: currentAppUserID, + idfa: identifierForAdvertisers, + network: network, + networkUserId: networkUserId) else { return } - var newDictToCache = latestNetworkIdsAndAdvertisingIdsSentByNetwork - newDictToCache[networkKey] = newValueForNetwork var newData = data if let identifierForAdvertisers = identifierForAdvertisers { @@ -88,7 +81,7 @@ class AttributionPoster { } if !newData.isEmpty { - if network == .appleSearchAds { + if network.isAppleSearchAdds { postSearchAds(newData: newData, network: network, appUserID: currentAppUserID, @@ -102,13 +95,13 @@ class AttributionPoster { } } + @available(*, deprecated) func postAppleSearchAdsAttributionIfNeeded() { guard attributionFetcher.isAuthorizedToPostSearchAds else { return } - let latestIdsSent = latestNetworkIdAndAdvertisingIdentifierSent(network: .appleSearchAds) - guard latestIdsSent == nil else { + guard self.latestNetworkIdAndAdvertisingIdentifierSent(network: .appleSearchAds) == nil else { return } @@ -130,6 +123,22 @@ 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() { + guard latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) == nil else { + return + } + + guard let attributionToken = attributionFetcher.adServicesToken else { + return + } + + self.post(adServicesToken: attributionToken) + } + func postPostponedAttributionDataIfNeeded() { guard let postponedAttributionData = Self.postponedAttributionData else { return @@ -154,35 +163,74 @@ 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 postSearchAds(newData: [String: Any], network: AttributionNetwork, appUserID: String, - newDictToCache: [String: String]) { + newDictToCache: [AttributionNetwork: String]) { backend.post(attributionData: newData, network: network, appUserID: appUserID) { error in guard error == nil else { return } - self.deviceCache.set(latestNetworkAndAdvertisingIdsSent: newDictToCache, appUserID: appUserID) + self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: appUserID) } } 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) + } + + 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/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index 87cf8c16b8..6e4a08940e 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -274,17 +274,33 @@ 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] ?? [:] + + let latestSent: [AttributionNetwork: String] = + 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( + networkKey: networkKey + ) + ) + return nil + } + return attributionNetwork + } + + return latestSent } } - func set(latestNetworkAndAdvertisingIdsSent: [String: String], appUserID: String) { + func set(latestAdvertisingIdsByNetworkSent: [AttributionNetwork: String], appUserID: String) { self.userDefaults.write { - $0.setValue(latestNetworkAndAdvertisingIdsSent, + let latestAdIdsByRawNetworkStringSent = latestAdvertisingIdsByNetworkSent.mapKeys { String($0.rawValue) } + $0.setValue(latestAdIdsByRawNetworkStringSent, forKey: CacheKeyBases.attributionDataDefaults + appUserID) } } diff --git a/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md b/Sources/DocCDocumentation/DocCDocumentation.docc/Purchases.md index 5e6b18fbe5..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/automaticAppleSearchAdsAttributionCollection`` - ``Purchases/proxyURL`` - ``Purchases/verboseLogs`` - ``Purchases/verboseLogHandler`` diff --git a/Sources/Logging/Strings/AttributionStrings.swift b/Sources/Logging/Strings/AttributionStrings.swift index 7b75f7b0a6..c05c2dfff9 100644 --- a/Sources/Logging/Strings/AttributionStrings.swift +++ b/Sources/Logging/Strings/AttributionStrings.swift @@ -38,7 +38,11 @@ enum AttributionStrings { case unsynced_attributes(unsyncedAttributes: SubscriberAttribute.Dictionary) 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) + case adservices_token_post_failed(error: BackendError) + case adservices_token_post_succeeded + case latest_attribution_sent_user_defaults_invalid(networkKey: String) } @@ -115,8 +119,21 @@ 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)" + + 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(let networkKey): + return "Attribution data stored in UserDefaults has invalid format for network key: \(networkKey)" } } diff --git a/Sources/Logging/Strings/StoreKitStrings.swift b/Sources/Logging/Strings/StoreKitStrings.swift index 657a261af3..a69e905b9e 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) + case sk1_discount_missing_locale case no_cached_products_starting_store_products_request(identifiers: Set) @@ -76,6 +78,9 @@ extension StoreKitStrings: CustomStringConvertible { 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)" + case .sk1_discount_missing_locale: return "There is an issue with the App Store, this SKProductDiscount is missing a Locale - " + "The current device Locale will be used instead." diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index b70832762f..c13d125bd7 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -144,6 +144,12 @@ public extension Purchases { ) } + /** + * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. + */ + @available(*, deprecated, message: "Use Purchases.shared.attribution.enableAdServicesAttributionTokenCollection() instead") + @objc static var automaticAppleSearchAdsAttributionCollection: Bool = false + } public extension Purchases { diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index 9ecd726b02..2ba308985d 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -68,6 +68,14 @@ class Backend { completion: completion) } + func post(adServicesToken: String, + appUserID: String, + completion: CustomerAPI.SimpleResponseHandler?) { + self.customer.post(adServicesToken: adServicesToken, + appUserID: appUserID, + completion: completion) + } + func getCustomerInfo(appUserID: String, completion: @escaping CustomerAPI.CustomerInfoResponseHandler) { self.customer.getCustomerInfo(appUserID: appUserID, completion: completion) } diff --git a/Sources/Networking/CustomerAPI.swift b/Sources/Networking/CustomerAPI.swift index 6738bf8e39..bd2d29c7ef 100644 --- a/Sources/Networking/CustomerAPI.swift +++ b/Sources/Networking/CustomerAPI.swift @@ -64,6 +64,17 @@ class CustomerAPI { self.backendConfig.operationQueue.addOperation(postAttributionDataOperation) } + func post(adServicesToken: String, + appUserID: String, + completion: SimpleResponseHandler?) { + let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient, + appUserID: appUserID) + let postAttributionDataOperation = PostAdServicesTokenOperation(configuration: config, + token: adServicesToken, + responseHandler: completion) + self.backendConfig.operationQueue.addOperation(postAttributionDataOperation) + } + // swiftlint:disable:next function_parameter_count func post(receiptData: Data, appUserID: String, diff --git a/Sources/Networking/HTTPClient/HTTPRequest.swift b/Sources/Networking/HTTPClient/HTTPRequest.swift index 85a1c9c815..4a322166fa 100644 --- a/Sources/Networking/HTTPClient/HTTPRequest.swift +++ b/Sources/Networking/HTTPClient/HTTPRequest.swift @@ -68,6 +68,7 @@ extension HTTPRequest { case postOfferForSigning case postReceiptData case postSubscriberAttributes(appUserID: String) + case postAdServicesToken(appUserID: String) } @@ -94,6 +95,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..e212102905 --- /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: CustomerAPI.SimpleResponseHandler? + + init(configuration: UserSpecificConfiguration, + token: String, + responseHandler: CustomerAPI.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) { (response: HTTPResponse.Result) in + defer { + completion() + } + + self.responseHandler?(response.error.map(BackendError.networkError)) + } + } + +} + +private extension PostAdServicesTokenOperation { + + struct Body: Encodable { + + let aadAttributionToken: String + + init(aadAttributionToken: String) { + self.aadAttributionToken = aadAttributionToken + } + + } + +} diff --git a/Sources/Purchasing/Purchases/Attribution.swift b/Sources/Purchasing/Purchases/Attribution.swift index 36ead5945c..0242612337 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,28 @@ 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. + */ + @objc func enableAdServicesAttributionTokenCollection() { + self.automaticAdServicesAttributionTokenCollection = true + self.postAdServicesTokenIfNeeded() + } + + internal func postAdServicesTokenIfNeeded() { + if self.automaticAdServicesAttributionTokenCollection { + self.attributionPoster.postAdServicesTokenIfNeeded() + } + } + +} + public extension Attribution { /** diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 886b6ec98d..332bd2065e 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -91,11 +91,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void private weak var privateDelegate: PurchasesDelegate? private let operationDispatcher: OperationDispatcher - /** - * Enable automatic collection of Apple Search Ads attribution. Defaults to `false`. - */ - @objc public static var automaticAppleSearchAdsAttributionCollection: Bool = false - /** * Used to set the log level. Useful for debugging issues with the lovely team @RevenueCat. * @@ -308,13 +303,14 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void customerInfoManager: customerInfoManager, attributeSyncing: subscriberAttributesManager, appUserID: appUserID) - 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, @@ -466,9 +462,11 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void } else { Logger.warn(Strings.configure.autoSyncPurchasesDisabled) } - subscribeToAppStateNotifications() - attributionPoster.postPostponedAttributionDataIfNeeded() - postAppleSearchAddsAttributionCollectionIfNeeded() + + self.subscribeToAppStateNotifications() + self.attributionPoster.postPostponedAttributionDataIfNeeded() + + (self as DeprecatedSearchAdsAttribution).postAppleSearchAddsAttributionCollectionIfNeeded() self.customerInfoObservationDisposable = customerInfoManager.monitorChanges { [weak self] customerInfo in guard let self = self else { return } @@ -481,7 +479,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void storeKitWrapper.delegate = nil customerInfoObservationDisposable?() privateDelegate = nil - Self.automaticAppleSearchAdsAttributionCollection = false Self.proxyURL = nil } @@ -510,7 +507,8 @@ extension Purchases { attributionPoster.post(attributionData: data, fromNetwork: network, networkUserId: networkUserId) } - private func postAppleSearchAddsAttributionCollectionIfNeeded() { + @available(*, deprecated) + fileprivate func postAppleSearchAddsAttributionCollectionIfNeeded() { guard Self.automaticAppleSearchAdsAttributionCollection else { return } @@ -1657,7 +1655,14 @@ private extension Purchases { Logger.debug(Strings.configure.application_active) self.updateAllCachesIfNeeded() self.dispatchSyncSubscriberAttributes() - self.postAppleSearchAddsAttributionCollectionIfNeeded() + + (self as DeprecatedSearchAdsAttribution).postAppleSearchAddsAttributionCollectionIfNeeded() + +#if os(iOS) || os(macOS) + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + self.attribution.postAdServicesTokenIfNeeded() + } +#endif } @objc func applicationWillResignActive(notification: Notification) { @@ -1710,3 +1715,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/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 2d12822931..a4f79a34e0 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 */; }; @@ -48,6 +49,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; }; 570FAF4E2864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCNonSubscriptionTransactionAPI.h; sourceTree = ""; }; 570FAF4F2864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCNonSubscriptionTransactionAPI.m; sourceTree = ""; }; @@ -104,6 +106,7 @@ buildActionMask = 2147483647; files = ( 575885A42748274E00CA2169 /* RevenueCat.framework in Frameworks */, + 2C396F5E281C64B700669657 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -180,6 +183,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/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/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 16a3444a37..8d6ad5292d 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -74,7 +74,10 @@ + (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.shared.attribution.enableAdServicesAttributionTokenCollection instead automaticAppleSearchAdsAttributionCollection = [RCPurchases automaticAppleSearchAdsAttributionCollection]; // should have deprecation warning 'debugLogsEnabled' is deprecated: use logLevel instead diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj index 3d521d539d..cd4fef303f 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 */; }; @@ -48,6 +49,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; }; 570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSubscriptionTransactionAPI.swift; sourceTree = ""; }; 5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductDiscountAPI.swift; sourceTree = ""; }; @@ -81,6 +83,7 @@ buildActionMask = 2147483647; files = ( 5758859C2748272A00CA2169 /* RevenueCat.framework in Frameworks */, + 2C396F5C281C64AF00669657 /* AdServices.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,6 +93,7 @@ 2D6F3871270E58DB002C9987 /* Frameworks */ = { isa = PBXGroup; children = ( + 2C396F5B281C64AF00669657 /* AdServices.framework */, 5758859B2748272A00CA2169 /* RevenueCat.framework */, 575885972748271100CA2169 /* RevenueCat.framework */, ); 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/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 73ac068e44..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 automaticAppleSearchAdsAttributionCollection: Bool = Purchases.automaticAppleSearchAdsAttributionCollection let logLevel: LogLevel = Purchases.logLevel let proxyUrl: URL? = Purchases.proxyURL let forceUniversalAppStore: Bool = Purchases.forceUniversalAppStore @@ -87,8 +86,8 @@ private func checkStaticMethods() { let sharedPurchases: Purchases = Purchases.shared let isPurchasesConfigured: Bool = Purchases.isConfigured - print(canI, version, automaticAppleSearchAdsAttributionCollection, logLevel, proxyUrl!, - forceUniversalAppStore, simulatesAskToBuyInSandbox, sharedPurchases, isPurchasesConfigured) + print(canI, version, logLevel, proxyUrl!, forceUniversalAppStore, simulatesAskToBuyInSandbox, + sharedPurchases, isPurchasesConfigured) } private func checkTypealiases( @@ -263,6 +262,8 @@ 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/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift index fd50bb5d79..6fdf6129fa 100644 --- a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift +++ b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -73,8 +73,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/Attribution/AttributionPosterTests.swift b/Tests/UnitTests/Attribution/AttributionPosterTests.swift index b0c3df1d2b..25c5936b67 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)!) self.deviceCache.cache(appUserID: userID) self.backend = MockBackend() - self.attributionFetcher = AttributionFetcher(attributionFactory: attributionFactory, systemInfo: systemInfo) + self.attributionFetcher = MockAttributionFetcher(attributionFactory: attributionFactory, systemInfo: systemInfo) self.subscriberAttributesManager = MockSubscriberAttributesManager( backend: self.backend, deviceCache: self.deviceCache, @@ -58,25 +56,29 @@ class AttributionPosterTests: TestCase { subscriberAttributesManager: self.subscriberAttributesManager) self.resetAttributionStaticProperties() self.backend.stubbedPostAttributionDataCompletionResult = (nil, ()) + self.backend.stubbedPostAdServicesTokenCompletionResult = .success(()) } private func resetAttributionStaticProperties() { if #available(iOS 14, macOS 11, tvOS 14, *) { MockTrackingManagerProxy.mockAuthorizationStatus = .authorized } - MockAttributionTypeFactory.shouldReturnAdClientProxy = true MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true MockAdClientProxy.requestAttributionDetailsCallCount = 0 } override func tearDown() { - super.tearDown() UserDefaults.standard.removePersistentDomain(forName: userDefaultsSuiteName) UserDefaults.standard.synchronize() resetAttributionStaticProperties() + super.tearDown() } +} + +class AttributionPosterTests: BaseAttributionPosterTests { + func testPostAttributionDataSkipsIfAlreadySent() { let userID = "userID" backend.stubbedPostAttributionDataCompletionResult = (nil, ()) @@ -91,9 +93,9 @@ class AttributionPosterTests: TestCase { networkUserId: userID) expect(self.backend.invokedPostAttributionDataCount) == 0 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 - } + @available(*, deprecated) func testPostAppleSearchAdsAttributionDataSkipsIfAlreadySent() { let userID = "userID" backend.stubbedPostAttributionDataCompletionResult = (nil, ()) @@ -109,7 +111,6 @@ class AttributionPosterTests: TestCase { networkUserId: userID) expect(self.backend.invokedPostAttributionDataCount) == 1 expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 - } func testPostAttributionDataDoesntSkipIfNetworkChanged() { @@ -130,23 +131,7 @@ class AttributionPosterTests: TestCase { 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 - } - + @available(*, deprecated) func testPostAppleSearchAdsAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { backend.stubbedPostAttributionDataCompletionResult = (nil, ()) @@ -164,19 +149,24 @@ class AttributionPosterTests: TestCase { expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 } - func testPostAppleSearchAdsAttributionIfNeededSkipsIfATTFrameworkNotIncludedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } + func testPostAttributionDataDoesntSkipIfDifferentUserIdButSameNetwork() { + backend.stubbedPostAttributionDataCompletionResult = (nil, ()) - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true - MockAttributionTypeFactory.shouldReturnAdClientProxy = true - MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = false + attributionPoster.post(attributionData: ["something": "here"], + fromNetwork: .adjust, + networkUserId: "attributionUser1") + expect(self.backend.invokedPostAttributionDataCount) == 0 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 1 - self.attributionPoster.postAppleSearchAdsAttributionIfNeeded() + attributionPoster.post(attributionData: ["something": "else"], + fromNetwork: .adjust, + networkUserId: "attributionUser2") - expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 - expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 0 + expect(self.backend.invokedPostAttributionDataCount) == 0 + expect(self.subscriberAttributesManager.invokedConvertAttributionDataAndSetCount) == 2 } + @available(*, deprecated) func testPostAppleSearchAdsAttributionIfNeededSkipsIfIAdFrameworkNotIncluded() { MockAttributionTypeFactory.shouldReturnAdClientProxy = false MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true @@ -186,12 +176,26 @@ class AttributionPosterTests: TestCase { expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 0 } - // `MockTrackingManagerProxy.mockAuthorizationStatus isn't available on tvOS - #if os(iOS) +} - func testPostAppleSearchAdsAttributionIfNeededPostsIfATTFrameworkNotIncludedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } +#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 @@ -202,8 +206,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true MockTrackingManagerProxy.mockAuthorizationStatus = .authorized @@ -216,8 +218,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthorizedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false MockTrackingManagerProxy.mockAuthorizationStatus = .authorized MockAttributionTypeFactory.shouldReturnAdClientProxy = true @@ -229,8 +229,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededPostsIfAuthNotDeterminedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined MockAttributionTypeFactory.shouldReturnAdClientProxy = true @@ -242,8 +240,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededSkipsIfAuthNotDeterminedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true MockTrackingManagerProxy.mockAuthorizationStatus = .notDetermined @@ -256,8 +252,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnOldOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = false MockTrackingManagerProxy.mockAuthorizationStatus = .denied MockAttributionTypeFactory.shouldReturnAdClientProxy = true @@ -269,8 +263,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededSkipsIfNotAuthorizedOnNewOS() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - systemInfo.stubbedIsOperatingSystemAtLeastVersion = true MockTrackingManagerProxy.mockAuthorizationStatus = .denied MockAttributionTypeFactory.shouldReturnAdClientProxy = true @@ -282,8 +274,6 @@ class AttributionPosterTests: TestCase { } func testPostAppleSearchAdsAttributionIfNeededSkipsIfAlreadySent() throws { - guard #available(iOS 14, *) else { throw XCTSkip() } - MockTrackingManagerProxy.mockAuthorizationStatus = .authorized MockAttributionTypeFactory.shouldReturnAdClientProxy = true MockAttributionTypeFactory.shouldReturnTrackingManagerProxy = true @@ -296,6 +286,64 @@ class AttributionPosterTests: TestCase { expect(MockAdClientProxy.requestAttributionDetailsCallCount) == 1 } +} +#endif + +#if canImport(AdServices) +@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(()) + + 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 + } + + 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 } +#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] + } + } diff --git a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift index 636ee45921..2179d6748c 100644 --- a/Tests/UnitTests/Mocks/MockAttributionFetcher.swift +++ b/Tests/UnitTests/Mocks/MockAttributionFetcher.swift @@ -15,9 +15,11 @@ 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 + var adServicesTokenToReturn: String? = "mockAdServicesToken" + override var adServicesToken: String? { + adServicesTokenCollectionCalled = true + return adServicesTokenToReturn } + } diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index 08e3d2d83f..f970d53663 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -113,11 +113,29 @@ class MockBackend: Backend { } } + 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: CustomerAPI.SimpleResponseHandler?) { + invokedPostAdServicesToken = true + invokedPostAdServicesTokenCount += 1 + invokedPostAdServicesTokenParameters = (adServicesToken, appUserID) + invokedPostAdServicesTokenParametersList.append((adServicesToken, appUserID)) + if let result = stubbedPostAdServicesTokenCompletionResult { + completion?(result.error) + } + } + var invokedPostSubscriberAttributes = false var invokedPostSubscriberAttributesCount = 0 var invokedPostSubscriberAttributesParameters: (subscriberAttributes: [String: SubscriberAttribute]?, appUserID: String?)? var invokedPostSubscriberAttributesParametersList: [InvokedPostSubscriberAttributesParams] = [] - var stubbedPostSubscriberAttributesCompletionResult: (BackendError?, Void)? + var stubbedPostSubscriberAttributesCompletionResult: Result? override func post(subscriberAttributes: SubscriberAttribute.Dictionary, appUserID: String, @@ -129,7 +147,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 07d9234d5f..836310b53d 100644 --- a/Tests/UnitTests/Mocks/MockDeviceCache.swift +++ b/Tests/UnitTests/Mocks/MockDeviceCache.swift @@ -227,4 +227,24 @@ class MockDeviceCache: DeviceCache { 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/BackendPostAttributionDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift index 14616130f2..90650eca50 100644 --- a/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendPostAttributionDataTests.swift @@ -32,7 +32,7 @@ class BackendPostAttributionDataTests: BaseBackendTests { let data: [String: AnyObject] = ["a": "b" as NSString, "c": "d" as NSString] backend.post(attributionData: data, - network: AttributionNetwork.appleSearchAds, + network: .adjust, appUserID: Self.userID, completion: nil) 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/iOS14-testPostAdServicesCallsHttpClient.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAdServicesTokenTests/iOS14-testPostAdServicesCallsHttpClient.1.json new file mode 100644 index 0000000000..d3a2b02590 --- /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..d3a2b02590 --- /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/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 index 8372d04da1..a6115f95fb 100644 --- a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-testPostAttributesPutsDataInDataKey.1.json +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostAttributionDataTests/iOS12-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/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 9e1335b900..4832a8ae8b 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, @@ -126,14 +127,14 @@ class BasePurchasesTests: TestCase { var purchases: Purchases! func setupPurchases(automaticCollection: Bool = false) { - Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = automaticCollection self.identityManager.mockIsAnonymous = false self.initializePurchasesInstance(appUserId: self.identityManager.currentAppUserID) } func setupAnonPurchases() { - Purchases.automaticAppleSearchAdsAttributionCollection = false + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = false self.identityManager.mockIsAnonymous = true self.initializePurchasesInstance(appUserId: nil) } @@ -380,7 +381,6 @@ extension BasePurchasesTests { completion?(result.0) } } - } } diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift index e98f8c0ef8..170ef41cc8 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesAttributionDataTests.swift @@ -35,6 +35,7 @@ class PurchasesAttributionDataTests: BasePurchasesTests { expect(attributionData["rc_idfv"] as? String) == "rc_idfv" } + @available(*, deprecated) func testPassesTheArrayForAllNetworks() { self.setupPurchases() @@ -73,6 +74,7 @@ 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] @@ -97,6 +99,7 @@ class PurchasesAttributionDataTests: BasePurchasesTests { expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID } + @available(*, deprecated) func testAttributionDataDontSendNetworkAppUserIdIfNotProvided() throws { let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] @@ -116,6 +119,7 @@ class PurchasesAttributionDataTests: BasePurchasesTests { expect(invokedMethodParams.appUserID) == identityManager.currentAppUserID } + @available(*, deprecated) func testAdClientAttributionDataIsAutomaticallyCollected() throws { self.setupPurchases(automaticCollection: true) 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 3ff9391ece..9deb132e55 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, @@ -134,7 +135,8 @@ class PurchasesSubscriberAttributesTests: TestCase { } func setupPurchases(automaticCollection: Bool = false) { - Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection + Purchases.deprecated.automaticAppleSearchAdsAttributionCollection = automaticCollection + self.mockIdentityManager.mockIsAnonymous = false let purchasesOrchestrator = PurchasesOrchestrator(productsManager: mockProductsManager, storeKitWrapper: mockStoreKitWrapper, diff --git a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributesManagerTests.swift index 2f6516eeaa..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 = (mockError, ()) + mockBackend.stubbedPostSubscriberAttributesCompletionResult = .failure(mockError) self.subscriberAttributesManager.syncAttributesForAllUsers(currentAppUserID: currentUserID) expect(self.mockDeviceCache.invokedDeleteAttributesIfSyncedCount).toEventually(equal(0))