From 444a9797da4bf91c38bdac3523d379ae806562c7 Mon Sep 17 00:00:00 2001 From: Joshua Liebowitz Date: Mon, 23 Aug 2021 14:00:00 -0700 Subject: [PATCH] Migrates RCPurchases (#758) * Start of migration * Migrates RCPurchases Resolves #546 (last file to be migrated) * Use Nimble's throwAssertion predicate * Update for comments * Start of migration * Sorted project and files. * Fix build target for IntroEligibilityCalculatorTests * Remove debug print * Add back `sharedPurchases` --- PublicSDKAPITester/RCEntitlementInfoAPI.m | 2 - PublicSDKAPITester/RCEntitlementInfosAPI.m | 1 - PublicSDKAPITester/RCIntroEligibilityAPI.m | 2 +- PublicSDKAPITester/RCOfferingAPI.m | 2 +- PublicSDKAPITester/RCOfferingsAPI.m | 2 +- PublicSDKAPITester/RCPurchasesAPI.m | 10 +- PublicSDKAPITester/RCPurchasesErrorUtilsAPI.m | 2 +- PublicSDKAPITester/RCTransactionAPI.m | 1 - Purchases.xcodeproj/project.pbxproj | 142 +- .../RCPurchases+Protected.h | 68 - Purchases/Public/Purchases.h | 2 - Purchases/Public/RCPurchases.h | 627 -------- Purchases/Public/RCPurchases.m | 799 ---------- .../Identity/PurchaserInfoManager.swift | 2 - .../Logging/Strings/PurchaseStrings.swift | 8 + .../Misc/PurchasesTestStandIn.swift | 22 + PurchasesCoreSwift/Public/Purchases.swift | 1284 +++++++++++++++++ .../Public/PurchasesDelegate.swift | 49 + PurchasesCoreSwift/PurchasesCoreSwift.h | 51 + .../Purchasing/OfferingsManager.swift | 2 - .../Purchasing/PurchasesOrchestrator.swift | 7 +- .../NSData+RCExtensionsTests.swift | 1 - .../Misc/SystemInfoTests.swift | 2 +- .../Networking/ETagManagerTests.swift | 1 - .../Networking/HTTPClientTests.swift | 2 +- .../Purchasing/OfferingsTests.swift | 3 +- .../Purchasing/StoreKitWrapperTests.swift | 3 +- .../Misc/ISOPeriodFormatterTests.swift | 5 +- .../Mocks/MockNotificationCenter.swift | 2 + .../Mocks/MockOfferingsFactory.swift | 1 + .../Mocks/MockPaymentDiscount.swift | 1 + .../Mocks/MockPurchasesDelegate.swift | 19 +- PurchasesTests/Mocks/MockSKDiscount.swift | 1 + PurchasesTests/Mocks/MockSKProduct.swift | 2 + .../Mocks/MockStoreKitWrapper.swift | 1 + PurchasesTests/Mocks/MockTransaction.swift | 2 + ...roductSubscriptionDurationExtensions.swift | 1 + .../PurchasesTests-Bridging-Header.h | 6 +- .../Purchasing/PurchasesTests.swift | 191 ++- .../PurchasesSubscriberAttributesTests.swift | 21 +- RCPurchaserInfoAPI.m | 1 - StoreKitTests/StoreKitTests.swift | 2 +- 42 files changed, 1648 insertions(+), 1705 deletions(-) delete mode 100644 Purchases/ProtectedExtensions/RCPurchases+Protected.h delete mode 100644 Purchases/Public/RCPurchases.h delete mode 100644 Purchases/Public/RCPurchases.m create mode 100644 PurchasesCoreSwift/Misc/PurchasesTestStandIn.swift create mode 100644 PurchasesCoreSwift/Public/Purchases.swift create mode 100644 PurchasesCoreSwift/Public/PurchasesDelegate.swift diff --git a/PublicSDKAPITester/RCEntitlementInfoAPI.m b/PublicSDKAPITester/RCEntitlementInfoAPI.m index b02ddd04b4..996c0915c2 100644 --- a/PublicSDKAPITester/RCEntitlementInfoAPI.m +++ b/PublicSDKAPITester/RCEntitlementInfoAPI.m @@ -6,8 +6,6 @@ // Copyright © 2021 Purchases. All rights reserved. // - -@import Purchases; @import PurchasesCoreSwift; #import "RCEntitlementInfoAPI.h" diff --git a/PublicSDKAPITester/RCEntitlementInfosAPI.m b/PublicSDKAPITester/RCEntitlementInfosAPI.m index 9b878f90ea..3fafb00114 100644 --- a/PublicSDKAPITester/RCEntitlementInfosAPI.m +++ b/PublicSDKAPITester/RCEntitlementInfosAPI.m @@ -6,7 +6,6 @@ // Copyright © 2021 Purchases. All rights reserved. // -@import Purchases; @import PurchasesCoreSwift; #import "RCEntitlementInfosAPI.h" diff --git a/PublicSDKAPITester/RCIntroEligibilityAPI.m b/PublicSDKAPITester/RCIntroEligibilityAPI.m index 6396d43a0f..2b9f7719ee 100644 --- a/PublicSDKAPITester/RCIntroEligibilityAPI.m +++ b/PublicSDKAPITester/RCIntroEligibilityAPI.m @@ -6,7 +6,7 @@ // Copyright © 2021 Purchases. All rights reserved. // -@import Purchases; +@import PurchasesCoreSwift; #import "RCIntroEligibilityAPI.h" @implementation RCIntroEligibilityAPI diff --git a/PublicSDKAPITester/RCOfferingAPI.m b/PublicSDKAPITester/RCOfferingAPI.m index eab2d92da7..bd7d145f79 100644 --- a/PublicSDKAPITester/RCOfferingAPI.m +++ b/PublicSDKAPITester/RCOfferingAPI.m @@ -6,7 +6,7 @@ // Copyright © 2021 Purchases. All rights reserved. // -@import Purchases; +@import PurchasesCoreSwift; #import "RCOfferingAPI.h" @implementation RCOfferingAPI diff --git a/PublicSDKAPITester/RCOfferingsAPI.m b/PublicSDKAPITester/RCOfferingsAPI.m index 938ae683b9..9e2af7cb88 100644 --- a/PublicSDKAPITester/RCOfferingsAPI.m +++ b/PublicSDKAPITester/RCOfferingsAPI.m @@ -6,7 +6,7 @@ // Copyright © 2021 Purchases. All rights reserved. // -@import Purchases; +@import PurchasesCoreSwift; #import "RCOfferingsAPI.h" diff --git a/PublicSDKAPITester/RCPurchasesAPI.m b/PublicSDKAPITester/RCPurchasesAPI.m index 0b2dd913cf..b74cee4a18 100644 --- a/PublicSDKAPITester/RCPurchasesAPI.m +++ b/PublicSDKAPITester/RCPurchasesAPI.m @@ -5,7 +5,6 @@ // Created by Joshua Liebowitz on 6/18/21. // -@import Purchases; @import PurchasesCoreSwift; @import StoreKit; @@ -43,6 +42,9 @@ + (void)checkAPI { // TODO: iOS ONLY, TEST FOR THIS API BY LOOKING UP SELECTOR // [p presentCodeRedemptionSheet]; RCPurchases *p = [RCPurchases configureWithAPIKey:@""]; + [RCPurchases configureWithAPIKey:@"" appUserID:@""]; + [RCPurchases configureWithAPIKey:@"" appUserID:@"" observerMode:false]; + [RCPurchases configureWithAPIKey:@"" appUserID:@"" observerMode:false userDefaults:nil]; [RCPurchases setLogHandler:^(RCLogLevel l, NSString *i) {}]; canI = [RCPurchases canMakePayments]; @@ -70,10 +72,7 @@ + (void)checkAPI { SKPaymentDiscount *skmd = [[SKPaymentDiscount alloc] init]; RCPackage *pack = [[RCPackage alloc] initWithIdentifier:@"" packageType:RCPackageTypeCustom product:skp offeringIdentifier:@""]; - [RCPurchases configureWithAPIKey:@"" appUserID:@""]; - [RCPurchases configureWithAPIKey:@"" appUserID:@"" observerMode:false]; - [RCPurchases configureWithAPIKey:@"" appUserID:@"" observerMode:false userDefaults:nil]; - + [p invalidatePurchaserInfoCache]; NSDictionary *attributes = nil; @@ -103,7 +102,6 @@ + (void)checkAPI { [p restoreTransactionsWithCompletionBlock:^(RCPurchaserInfo *i, NSError *e) {}]; [p syncPurchasesWithCompletionBlock:^(RCPurchaserInfo *i, NSError *e) {}]; [p checkTrialOrIntroductoryPriceEligibility:@[@""] completionBlock:^(NSDictionary *d) { }]; - [p checkTrialOrIntroductoryPriceEligibility:@[@""] completionBlock:^(NSDictionary *r) { }]; [p paymentDiscountForProductDiscount:skpd product:skp completion:^(SKPaymentDiscount *d, NSError *e) { }]; [p purchaseProduct:skp withDiscount:skmd completionBlock:^(SKPaymentTransaction *t, RCPurchaserInfo *i, NSError *e, BOOL userCancelled) { }]; [p purchasePackage:pack withDiscount:skmd completionBlock:^(SKPaymentTransaction *t, RCPurchaserInfo *i, NSError *e, BOOL userCancelled) { }]; diff --git a/PublicSDKAPITester/RCPurchasesErrorUtilsAPI.m b/PublicSDKAPITester/RCPurchasesErrorUtilsAPI.m index 4b181d08cb..f1288ca093 100644 --- a/PublicSDKAPITester/RCPurchasesErrorUtilsAPI.m +++ b/PublicSDKAPITester/RCPurchasesErrorUtilsAPI.m @@ -11,8 +11,8 @@ // // Created by César de la Vega on 7/21/21. -@import Purchases; @import PurchasesCoreSwift; +@import StoreKit; #import "RCPurchasesErrorUtilsAPI.h" diff --git a/PublicSDKAPITester/RCTransactionAPI.m b/PublicSDKAPITester/RCTransactionAPI.m index cf4ac40168..2d48ff130d 100644 --- a/PublicSDKAPITester/RCTransactionAPI.m +++ b/PublicSDKAPITester/RCTransactionAPI.m @@ -6,7 +6,6 @@ // Copyright © 2021 Purchases. All rights reserved. // -@import Purchases; @import PurchasesCoreSwift; #import "RCTransactionAPI.h" diff --git a/Purchases.xcodeproj/project.pbxproj b/Purchases.xcodeproj/project.pbxproj index fd39e0e082..ff3d284a63 100644 --- a/Purchases.xcodeproj/project.pbxproj +++ b/Purchases.xcodeproj/project.pbxproj @@ -64,7 +64,6 @@ 2DDF41CE24F6F4C3005BC22D /* InAppPurchaseBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41C524F6F4C3005BC22D /* InAppPurchaseBuilderTests.swift */; }; 2DDF41CF24F6F4C3005BC22D /* ReceiptParsing+TestsWithRealReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41C724F6F4C3005BC22D /* ReceiptParsing+TestsWithRealReceipts.swift */; }; 2DDF41DA24F6F4DB005BC22D /* ReceiptParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351D0EBC4698E1D3585A6 /* ReceiptParserTests.swift */; }; - 2DDF41DB24F6F4DB005BC22D /* IntroEligibilityCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */; }; 2DDF41DC24F6F4DB005BC22D /* ProductsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351F0E21361EAEC078A0D /* ProductsManagerTests.swift */; }; 2DDF41DE24F6F527005BC22D /* MockAppleReceiptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C1554F296F7F1317747 /* MockAppleReceiptBuilder.swift */; }; 2DDF41DF24F6F527005BC22D /* MockProductsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C9439E087F63ECC4F59 /* MockProductsManager.swift */; }; @@ -89,8 +88,6 @@ 2DE61A84264190830021CEA0 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE61A83264190830021CEA0 /* Constants.swift */; }; 2DE63BB324EC73F3001288D6 /* PurchasesCoreSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DC5621624EC63420031F69B /* PurchasesCoreSwift.framework */; }; 2DEB976B247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */; }; - 350FBDE91F7EEF070065833D /* RCPurchases.h in Headers */ = {isa = PBXBuildFile; fileRef = 350FBDE71F7EEF070065833D /* RCPurchases.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 350FBDEA1F7EEF070065833D /* RCPurchases.m in Sources */ = {isa = PBXBuildFile; fileRef = 350FBDE81F7EEF070065833D /* RCPurchases.m */; }; 35262A0F1F7C4B9100C04F2C /* Purchases.h in Headers */ = {isa = PBXBuildFile; fileRef = 35262A011F7C4B9100C04F2C /* Purchases.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35262A231F7D77E600C04F2C /* Purchases.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 352629FE1F7C4B9100C04F2C /* Purchases.framework */; }; 35272E1B26D0029300F22C3B /* DeviceCacheSubscriberAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C4A795A0F056381A1B3 /* DeviceCacheSubscriberAttributesTests.swift */; }; @@ -119,7 +116,6 @@ 35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */; }; 35F82BB426A9A74D0051DF03 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */; }; 35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB526A9B8030051DF03 /* AttributionDataMigrator.swift */; }; - 37E3500156786798CB166571 /* RCPurchases+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 37E352DAD631B3A45C041148 /* RCPurchases+Protected.h */; settings = {ATTRIBUTES = (Private, ); }; }; 37E350C67712B9E054FEF297 /* AttributionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35CD16BB73BB091E64D9A /* AttributionData.swift */; }; 37E351A24D613F0DFE6AF05F /* MockBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35645928A0009F4C105A7 /* MockBackend.swift */; }; 37E351AB03EE37534CA10B59 /* MockInMemoryCachedOfferings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C5A65AAF701DED59800 /* MockInMemoryCachedOfferings.swift */; }; @@ -170,6 +166,9 @@ A56F9AB126990E9200AFC48F /* PurchaserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56F9AB026990E9200AFC48F /* PurchaserInfo.swift */; }; A5C7008D26AB647B00B2CA64 /* RCOfferingsAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = B3B5FBB9269D100400104A0C /* RCOfferingsAPI.m */; }; A5C7009026AF5A7900B2CA64 /* RCPurchaserInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A56F9AAE26990A3A00AFC48F /* RCPurchaserInfoAPI.m */; }; + B300E4BF26D436F900B22262 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; }; + B300E4C026D4371200B22262 /* SKPaymentTransactionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492526B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift */; }; + B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */; }; B3083A11269931CF007B5503 /* RCOfferingAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = B3083A10269931CF007B5503 /* RCOfferingAPI.m */; }; B3083A132699334C007B5503 /* Offering.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3083A122699334C007B5503 /* Offering.swift */; }; B319514926C19856002CA9AC /* NSData+RCExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35ABEE9FD79CCA64E4F8B /* NSData+RCExtensionsTests.swift */; }; @@ -184,6 +183,9 @@ B34D2AA126960A3200D88C3A /* RCIntroEligibilityAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = B3DF6A4D269522040030D57C /* RCIntroEligibilityAPI.m */; }; B34D2AA4269649EC00D88C3A /* MockProductDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3572040A16F10B957563A /* MockProductDiscount.swift */; }; B34D2AA626976FC700D88C3A /* ErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34D2AA526976FC700D88C3A /* ErrorCode.swift */; }; + B35042C426CDB79A00905B95 /* Purchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35042C326CDB79A00905B95 /* Purchases.swift */; }; + B35042C626CDD3B100905B95 /* PurchasesDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35042C526CDD3B100905B95 /* PurchasesDelegate.swift */; }; + B35042CB26D0457E00905B95 /* PurchasesTestStandIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35042CA26D0457E00905B95 /* PurchasesTestStandIn.swift */; }; B35F9E0926B4BEED00095C3F /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35F9E0826B4BEED00095C3F /* String+Extensions.swift */; }; B36824BE268FBC5B00957E4C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B36824BD268FBC5B00957E4C /* XCTest.framework */; }; B36824BF268FBC8700957E4C /* SubscriberAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */; }; @@ -229,7 +231,6 @@ F575858F26C0893600C12B97 /* MockOfferingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F575858E26C0893600C12B97 /* MockOfferingsManager.swift */; }; F575859026C0893E00C12B97 /* MockOfferingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F575858E26C0893600C12B97 /* MockOfferingsManager.swift */; }; F575859226C08E3F00C12B97 /* OfferingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F575859126C08E3F00C12B97 /* OfferingsManagerTests.swift */; }; - F591492626B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492526B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift */; }; F591492826B9956C00D32E58 /* MockTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492726B9956C00D32E58 /* MockTransaction.swift */; }; F5BE424026962ACF00254A30 /* ReceiptRefreshPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE423F26962ACF00254A30 /* ReceiptRefreshPolicy.swift */; }; F5BE424226965F9F00254A30 /* ProductInfoExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE424126965F9F00254A30 /* ProductInfoExtractor.swift */; }; @@ -376,8 +377,6 @@ 350A1B84226E3E8700CCA10F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 350FBDD71F7DF17F0065833D /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OHHTTPStubs.framework; path = Carthage/Build/iOS/OHHTTPStubs.framework; sourceTree = SOURCE_ROOT; }; 350FBDDC1F7EA3570065833D /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = SOURCE_ROOT; }; - 350FBDE71F7EEF070065833D /* RCPurchases.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCPurchases.h; sourceTree = ""; }; - 350FBDE81F7EEF070065833D /* RCPurchases.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCPurchases.m; sourceTree = ""; }; 352629FE1F7C4B9100C04F2C /* Purchases.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Purchases.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 35262A011F7C4B9100C04F2C /* Purchases.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Purchases.h; sourceTree = ""; }; 35262A021F7C4B9100C04F2C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -426,7 +425,6 @@ 37E35292137BBF2810CE4F4B /* MockHTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHTTPClient.swift; sourceTree = ""; }; 37E35294EBC1E5A879C95540 /* IdentityManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityManagerTests.swift; sourceTree = ""; }; 37E352B11676F7DC51559E84 /* MockReceiptFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockReceiptFetcher.swift; sourceTree = ""; }; - 37E352DAD631B3A45C041148 /* RCPurchases+Protected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCPurchases+Protected.h"; sourceTree = ""; }; 37E352F86A0A8EB05BAD77C4 /* StoreKitWrapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreKitWrapperTests.swift; sourceTree = ""; }; 37E353AF2CAD3CEDE6D9B368 /* NSError+RCExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSError+RCExtensionsTests.swift"; sourceTree = ""; }; 37E353CBE9CF2572A72A347F /* HTTPClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClientTests.swift; sourceTree = ""; }; @@ -509,6 +507,9 @@ B33CEA9D268BC39D008A3144 /* RCTransactionAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTransactionAPI.m; sourceTree = ""; }; B33CEA9F268CDCC9008A3144 /* ISOPeriodFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ISOPeriodFormatter.swift; sourceTree = ""; }; B34D2AA526976FC700D88C3A /* ErrorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCode.swift; sourceTree = ""; }; + B35042C326CDB79A00905B95 /* Purchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Purchases.swift; sourceTree = ""; }; + B35042C526CDD3B100905B95 /* PurchasesDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesDelegate.swift; sourceTree = ""; }; + B35042CA26D0457E00905B95 /* PurchasesTestStandIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesTestStandIn.swift; sourceTree = ""; }; B35F9E0826B4BEED00095C3F /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B36824BD268FBC5B00957E4C /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; B372EC53268FEDC60099171E /* PromotionalOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOffer.swift; sourceTree = ""; }; @@ -636,11 +637,11 @@ children = ( 2CD72941268A823900BFC976 /* Data+Extensions.swift */, 2CD72943268A826F00BFC976 /* Date+Extensions.swift */, - 2CD72947268A828400BFC976 /* Locale+Extensions.swift */, 37E353FD94A8FD5CD8796530 /* DateFormatter+Extensions.swift */, - B35F9E0826B4BEED00095C3F /* String+Extensions.swift */, 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */, B3E26A4926BE0A8E003ACCF3 /* Error+Extensions.swift */, + 2CD72947268A828400BFC976 /* Locale+Extensions.swift */, + B35F9E0826B4BEED00095C3F /* String+Extensions.swift */, ); path = FoundationExtensions; sourceTree = ""; @@ -667,14 +668,14 @@ isa = PBXGroup; children = ( 2D11F5E0250FF886005A70E8 /* AttributionStrings.swift */, - 9A65E09F2591A23200DE00B0 /* OfferingStrings.swift */, - 9A65E0A42591A23500DE00B0 /* PurchaseStrings.swift */, - 9A65E0A92591A23800DE00B0 /* RestoreStrings.swift */, + 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */, 9A65E0752591977200DE00B0 /* IdentityStrings.swift */, 9A65E07A2591977500DE00B0 /* NetworkStrings.swift */, - 9A65E07F2591977900DE00B0 /* ReceiptStrings.swift */, - 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */, + 9A65E09F2591A23200DE00B0 /* OfferingStrings.swift */, 9A65E03A25918B0900DE00B0 /* PurchaserInfoStrings.swift */, + 9A65E0A42591A23500DE00B0 /* PurchaseStrings.swift */, + 9A65E07F2591977900DE00B0 /* ReceiptStrings.swift */, + 9A65E0A92591A23800DE00B0 /* RestoreStrings.swift */, 37E3507939634ED5A9280544 /* Strings.swift */, ); path = Strings; @@ -683,11 +684,11 @@ 2D1A28CB24AA6F4B006BE931 /* LocalReceiptParsing */ = { isa = PBXGroup; children = ( - 2D5BB46A24C8E8ED00E27537 /* ReceiptParser.swift */, - 2D8F622224D30F9D00F993AA /* ReceiptParsingError.swift */, 37E35FCF87558ACB498521F1 /* BasicTypes */, 37E355596456B3DFA01EF081 /* Builders */, 37E35556F2D7B8B28B169C77 /* DataConverters */, + 2D5BB46A24C8E8ED00E27537 /* ReceiptParser.swift */, + 2D8F622224D30F9D00F993AA /* ReceiptParsingError.swift */, ); path = LocalReceiptParsing; sourceTree = ""; @@ -695,22 +696,22 @@ 2DC5621724EC63420031F69B /* PurchasesCoreSwift */ = { isa = PBXGroup; children = ( + F5BE44412698580200254A30 /* Attribution */, B3B5FBBD269E080A00104A0C /* Caching */, 2CD72940268A820E00BFC976 /* FoundationExtensions */, - F5E4383626B852D700841CC8 /* StoreKitExtensions */, - 2D1A28CB24AA6F4B006BE931 /* LocalReceiptParsing */, B3A36AAC26BC76230059EDEA /* Identity */, - 35D832CB262A5B3400E60AC5 /* Networking */, - 354895D0267AE32D001DC5B1 /* SubscriberAttributes */, + 2DC5621924EC63430031F69B /* Info.plist */, + 2D97458E24BDFCEF006245E9 /* IntroEligibilityCalculator.swift */, + 2D1A28CB24AA6F4B006BE931 /* LocalReceiptParsing */, 2D11F5DE250FF63E005A70E8 /* Logging */, 2DDA3E4524DB0B4500EDFE5B /* Misc */, + 35D832CB262A5B3400E60AC5 /* Networking */, B3DDB55726854850008CCF23 /* Public */, + 2DC5621824EC63430031F69B /* PurchasesCoreSwift.h */, 354235D524C11138008C84EE /* Purchasing */, + F5E4383626B852D700841CC8 /* StoreKitExtensions */, + 354895D0267AE32D001DC5B1 /* SubscriberAttributes */, B39E811B268E885900D31189 /* SubscriberAttributes */, - F5BE44412698580200254A30 /* Attribution */, - 2DC5621924EC63430031F69B /* Info.plist */, - 2D97458E24BDFCEF006245E9 /* IntroEligibilityCalculator.swift */, - 2DC5621824EC63430031F69B /* PurchasesCoreSwift.h */, 37E355CBB3F3A31A32687B14 /* Transaction.swift */, ); path = PurchasesCoreSwift; @@ -719,17 +720,17 @@ 2DC5622224EC63430031F69B /* PurchasesCoreSwiftTests */ = { isa = PBXGroup; children = ( - 35272E1A26D0023400F22C3B /* Misc */, + F5BE444626985E6E00254A30 /* Attribution */, 37E35AE0CDC4C2AA8260FB58 /* Caching */, - F591492426B994A100D32E58 /* StoreKitExtensions */, 35F82BBB26A9BFA60051DF03 /* FoundationExtensions */, 2DD02D5924AD128A00419CD9 /* LocalReceiptParsing */, + 35272E1A26D0023400F22C3B /* Misc */, 2DDF41DD24F6F4F9005BC22D /* Mocks */, - 354235D624C11160008C84EE /* Purchasing */, 35D832FE262FAD6900E60AC5 /* Networking */, + 354235D624C11160008C84EE /* Purchasing */, 2DDE559824C8B5D100DCB087 /* Resources */, + F591492426B994A100D32E58 /* StoreKitExtensions */, B36824BB268FBB9B00957E4C /* SubscriberAttributes */, - F5BE444626985E6E00254A30 /* Attribution */, 37E35A5970D1604E8C8011FC /* TestHelpers */, 2DC5622524EC63430031F69B /* Info.plist */, 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */, @@ -751,11 +752,12 @@ 2DDA3E4524DB0B4500EDFE5B /* Misc */ = { isa = PBXGroup; children = ( - 2DDA3E4624DB0B5400EDFE5B /* OperationDispatcher.swift */, 37E3567189CF6A746EE3CCC2 /* DateExtensions.swift */, 0313FD40268A506400168386 /* DateProvider.swift */, - B3AA6237268B926F00894871 /* SystemInfo.swift */, B33CEA9F268CDCC9008A3144 /* ISOPeriodFormatter.swift */, + 2DDA3E4624DB0B5400EDFE5B /* OperationDispatcher.swift */, + B35042CA26D0457E00905B95 /* PurchasesTestStandIn.swift */, + B3AA6237268B926F00894871 /* SystemInfo.swift */, ); path = Misc; sourceTree = ""; @@ -832,9 +834,9 @@ isa = PBXGroup; children = ( 2DE20B7926408921004C597D /* Configuration.storekit */, - 2DE20B6E264087FB004C597D /* StoreKitTests.swift */, - 2DE20B70264087FB004C597D /* Info.plist */, 2DE61A83264190830021CEA0 /* Constants.swift */, + 2DE20B70264087FB004C597D /* Info.plist */, + 2DE20B6E264087FB004C597D /* StoreKitTests.swift */, ); path = StoreKitTests; sourceTree = ""; @@ -842,11 +844,11 @@ 2DE20B8026409EB7004C597D /* StoreKitTestApp */ = { isa = PBXGroup; children = ( - 2DE20B8126409EB7004C597D /* StoreKitTestAppApp.swift */, - 2DE20B8326409EB7004C597D /* ContentView.swift */, + 2DE20B8726409EB8004C597D /* Preview Content */, 2DE20B8526409EB8004C597D /* Assets.xcassets */, + 2DE20B8326409EB7004C597D /* ContentView.swift */, 2DE20B8A26409EB8004C597D /* Info.plist */, - 2DE20B8726409EB8004C597D /* Preview Content */, + 2DE20B8126409EB7004C597D /* StoreKitTestAppApp.swift */, ); path = StoreKitTestApp; sourceTree = ""; @@ -872,8 +874,6 @@ isa = PBXGroup; children = ( 35262A011F7C4B9100C04F2C /* Purchases.h */, - 350FBDE71F7EEF070065833D /* RCPurchases.h */, - 350FBDE81F7EEF070065833D /* RCPurchases.m */, ); path = Public; sourceTree = ""; @@ -914,7 +914,6 @@ isa = PBXGroup; children = ( 35262A021F7C4B9100C04F2C /* Info.plist */, - 37E3529AD4F30D0B6D56853B /* ProtectedExtensions */, 350FBDE61F7EEEDF0065833D /* Public */, ); path = Purchases; @@ -966,20 +965,20 @@ 354235D524C11138008C84EE /* Purchasing */ = { isa = PBXGroup; children = ( - 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */, + 35549322269E298B005F9AE9 /* OfferingsFactory.swift */, + F575858C26C088FE00C12B97 /* OfferingsManager.swift */, B372EC55268FEF020099171E /* ProductInfo.swift */, + B3C1AEF9268FF4DB0013D50D /* ProductInfoEnums.swift */, + F5BE424126965F9F00254A30 /* ProductInfoExtractor.swift */, 37E35C7060D7E486F5958BED /* ProductsManager.swift */, 37E35E8DCF998D9DB63850F8 /* ProductsRequestFactory.swift */, B372EC53268FEDC60099171E /* PromotionalOffer.swift */, - 3597020F24BF6A710010506E /* TransactionsFactory.swift */, - B3C1AEF9268FF4DB0013D50D /* ProductInfoEnums.swift */, + 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */, + 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */, F5BE423F26962ACF00254A30 /* ReceiptRefreshPolicy.swift */, 2D991AC9268BA56900085481 /* StoreKitRequestFetcher.swift */, - F5BE424126965F9F00254A30 /* ProductInfoExtractor.swift */, 2D4E926426990AB1000E10B0 /* StoreKitWrapper.swift */, - 35549322269E298B005F9AE9 /* OfferingsFactory.swift */, - F575858C26C088FE00C12B97 /* OfferingsManager.swift */, - 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */, + 3597020F24BF6A710010506E /* TransactionsFactory.swift */, ); path = Purchasing; sourceTree = ""; @@ -1017,12 +1016,12 @@ isa = PBXGroup; children = ( B3C4AAD426B8911300E1B3C8 /* Backend.swift */, + 35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */, 35D832CC262A5B7500E60AC5 /* ETagManager.swift */, - 35D832F3262E606500E60AC5 /* HTTPResponse.swift */, + 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */, F5BE443C26977F9B00254A30 /* HTTPRequest.swift */, + 35D832F3262E606500E60AC5 /* HTTPResponse.swift */, 35D832D1262E56DB00E60AC5 /* HTTPStatusCodes.swift */, - 35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */, - 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */, B3766F1D26BDA95100141450 /* IntroEligibilityResponse.swift */, ); path = Networking; @@ -1086,14 +1085,6 @@ path = Mocks; sourceTree = ""; }; - 37E3529AD4F30D0B6D56853B /* ProtectedExtensions */ = { - isa = PBXGroup; - children = ( - 37E352DAD631B3A45C041148 /* RCPurchases+Protected.h */, - ); - path = ProtectedExtensions; - sourceTree = ""; - }; 37E35555C264F76E6CFFC2C8 /* Purchasing */ = { isa = PBXGroup; children = ( @@ -1211,8 +1202,6 @@ B387F46B2683FD730028701F /* PublicSDKAPITester */ = { isa = PBXGroup; children = ( - A56F9AAD26990A3A00AFC48F /* RCPurchaserInfoAPI.h */, - A56F9AAE26990A3A00AFC48F /* RCPurchaserInfoAPI.m */, B3D3C46E26856AFE00CB3C21 /* APITester-Bridging-Header.h */, B3D3C46F26856AFF00CB3C21 /* APITester.swift */, B387F46D2683FDAC0028701F /* main.m */, @@ -1228,12 +1217,14 @@ B3083A10269931CF007B5503 /* RCOfferingAPI.m */, B3B5FBB8269D100400104A0C /* RCOfferingsAPI.h */, B3B5FBB9269D100400104A0C /* RCOfferingsAPI.m */, + A56F9AAD26990A3A00AFC48F /* RCPurchaserInfoAPI.h */, + A56F9AAE26990A3A00AFC48F /* RCPurchaserInfoAPI.m */, B3AA359E268682410007771B /* RCPurchasesAPI.h */, B3AA359F268682410007771B /* RCPurchasesAPI.m */, - B33CEA9C268BC39D008A3144 /* RCTransactionAPI.h */, - B33CEA9D268BC39D008A3144 /* RCTransactionAPI.m */, 35F82BA526A848CB0051DF03 /* RCPurchasesErrorUtilsAPI.h */, 35F82BA726A849080051DF03 /* RCPurchasesErrorUtilsAPI.m */, + B33CEA9C268BC39D008A3144 /* RCTransactionAPI.h */, + B33CEA9D268BC39D008A3144 /* RCTransactionAPI.m */, ); path = PublicSDKAPITester; sourceTree = ""; @@ -1272,8 +1263,8 @@ B3B5FBBD269E080A00104A0C /* Caching */ = { isa = PBXGroup; children = ( - B3B5FBBE269E081E00104A0C /* InMemoryCachedObject.swift */, B3B5FBC0269E17CE00104A0C /* DeviceCache.swift */, + B3B5FBBE269E081E00104A0C /* InMemoryCachedObject.swift */, ); path = Caching; sourceTree = ""; @@ -1282,15 +1273,17 @@ isa = PBXGroup; children = ( B3B5FBB7269CED6D00104A0C /* Error Handling */, + B39E8119268E849900D31189 /* AttributionNetwork.swift */, B32B750026868C1D005647BF /* EntitlementInfo.swift */, B3AA6235268A81C700894871 /* EntitlementInfos.swift */, B3DF6A4F269524080030D57C /* IntroEligibility.swift */, - B3D3C4712685784800CB3C21 /* Package.swift */, - B3DDB55826854865008CCF23 /* PurchaseOwnershipType.swift */, - B39E8119268E849900D31189 /* AttributionNetwork.swift */, B3083A122699334C007B5503 /* Offering.swift */, B3B5FBBB269D121B00104A0C /* Offerings.swift */, + B3D3C4712685784800CB3C21 /* Package.swift */, + B3DDB55826854865008CCF23 /* PurchaseOwnershipType.swift */, A56F9AB026990E9200AFC48F /* PurchaserInfo.swift */, + B35042C326CDB79A00905B95 /* Purchases.swift */, + B35042C526CDD3B100905B95 /* PurchasesDelegate.swift */, ); path = Public; sourceTree = ""; @@ -1306,13 +1299,13 @@ F5BE44412698580200254A30 /* Attribution */ = { isa = PBXGroup; children = ( - F5BE44422698581100254A30 /* AttributionTypeFactory.swift */, F5BE4478269E4A4C00254A30 /* AdClientProxy.swift */, - F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */, F5BE447C269E4ADB00254A30 /* ASIdentifierManagerProxy.swift */, 37E35CD16BB73BB091E64D9A /* AttributionData.swift */, 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */, B3A55B7C26C452A7007EFC56 /* AttributionPoster.swift */, + F5BE44422698581100254A30 /* AttributionTypeFactory.swift */, + F5BE447A269E4A7500254A30 /* TrackingManagerProxy.swift */, ); path = Attribution; sourceTree = ""; @@ -1349,9 +1342,7 @@ buildActionMask = 2147483647; files = ( 2D22679125F2D9AD00E6950C /* PurchasesTests-Bridging-Header.h in Headers */, - 350FBDE91F7EEF070065833D /* RCPurchases.h in Headers */, 35262A0F1F7C4B9100C04F2C /* Purchases.h in Headers */, - 37E3500156786798CB166571 /* RCPurchases+Protected.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1507,7 +1498,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1250; ORGANIZATIONNAME = Purchases; TargetAttributes = { 2DC5621524EC63420031F69B = { @@ -1681,6 +1672,7 @@ 2D4E926526990AB1000E10B0 /* StoreKitWrapper.swift in Sources */, 2DDF419624F6F331005BC22D /* ProductsRequestFactory.swift in Sources */, 2DDF419724F6F331005BC22D /* DateExtensions.swift in Sources */, + B35042CB26D0457E00905B95 /* PurchasesTestStandIn.swift in Sources */, 9A65E0A52591A23500DE00B0 /* PurchaseStrings.swift in Sources */, B3C1AEFA268FF4DB0013D50D /* ProductInfoEnums.swift in Sources */, 2DC5623024EC63730031F69B /* OperationDispatcher.swift in Sources */, @@ -1709,6 +1701,7 @@ 2DDF419D24F6F331005BC22D /* IntroEligibilityCalculator.swift in Sources */, A525BF4B26C320D100C354C4 /* SubscriberAttributesManager.swift in Sources */, B3E26A4A26BE0A8E003ACCF3 /* Error+Extensions.swift in Sources */, + B35042C426CDB79A00905B95 /* Purchases.swift in Sources */, B3B5FBBF269E081E00104A0C /* InMemoryCachedObject.swift in Sources */, 9A65E0802591977900DE00B0 /* ReceiptStrings.swift in Sources */, B3DDB55926854865008CCF23 /* PurchaseOwnershipType.swift in Sources */, @@ -1749,6 +1742,7 @@ 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */, B372EC54268FEDC60099171E /* PromotionalOffer.swift in Sources */, 9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */, + B35042C626CDD3B100905B95 /* PurchasesDelegate.swift in Sources */, 0313FD41268A506400168386 /* DateProvider.swift in Sources */, B39E811A268E849900D31189 /* AttributionNetwork.swift in Sources */, 37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */, @@ -1770,10 +1764,11 @@ 2DDF41CB24F6F4C3005BC22D /* ASN1ObjectIdentifierBuilderTests.swift in Sources */, B36824BF268FBC8700957E4C /* SubscriberAttributeTests.swift in Sources */, 2DDF41CF24F6F4C3005BC22D /* ReceiptParsing+TestsWithRealReceipts.swift in Sources */, + B300E4C026D4371200B22262 /* SKPaymentTransactionExtensionsTests.swift in Sources */, 2DDF41C924F6F4C3005BC22D /* UInt8+ExtensionsTests.swift in Sources */, 2D4D6AF524F717B800B656BE /* ContainerFactory.swift in Sources */, + B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */, 2DDF41CA24F6F4C3005BC22D /* ArraySlice_UInt8+ExtensionsTests.swift in Sources */, - 2DDF41DB24F6F4DB005BC22D /* IntroEligibilityCalculatorTests.swift in Sources */, 2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */, 35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */, 2DDF41E224F6F527005BC22D /* MockProductsRequest.swift in Sources */, @@ -1803,8 +1798,8 @@ 2DDF41EA24F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift in Sources */, B36824C1268FBCE000957E4C /* MockDateProvider.swift in Sources */, 2DDF41CE24F6F4C3005BC22D /* InAppPurchaseBuilderTests.swift in Sources */, + B300E4BF26D436F900B22262 /* LogIntent.swift in Sources */, B319514926C19856002CA9AC /* NSData+RCExtensionsTests.swift in Sources */, - F591492626B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift in Sources */, 2D1C3F3926B9D8B800112626 /* MockBundle.swift in Sources */, 2DDF41E424F6F527005BC22D /* MockProductsRequestFactory.swift in Sources */, 35272E1F26D003F500F22C3B /* OfferingsTests.swift in Sources */, @@ -1837,7 +1832,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 350FBDEA1F7EEF070065833D /* RCPurchases.m in Sources */, 2D4C18A924F47E5000F268CD /* Purchases.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2220,6 +2214,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -2279,6 +2274,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; diff --git a/Purchases/ProtectedExtensions/RCPurchases+Protected.h b/Purchases/ProtectedExtensions/RCPurchases+Protected.h deleted file mode 100644 index 92c6f43488..0000000000 --- a/Purchases/ProtectedExtensions/RCPurchases+Protected.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// RCPurchases+Protected.h -// Purchases -// -// Created by RevenueCat. -// Copyright © 2019 RevenueCat. All rights reserved. -// - -@class RCPurchases, - RCStoreKitRequestFetcher, - RCBackend, - RCStoreKitWrapper, - RCReceiptFetcher, - RCAttributionFetcher, - RCAttributionPoster, - RCOfferingsFactory, - RCDeviceCache, - RCIdentityManager, - RCSubscriberAttributesManager, - RCSystemInfo, - RCOperationDispatcher, - RCIntroEligibilityCalculator, - RCReceiptParser, - RCPurchaserInfoManager, - RCProductsManager, - RCOfferingsManager; - -@protocol RCStoreKitWrapperDelegate; -#import "Purchases.h" - -NS_ASSUME_NONNULL_BEGIN - - -@interface RCPurchases (Protected) - -- (instancetype)initWithAppUserID:(nullable NSString *)appUserID - requestFetcher:(RCStoreKitRequestFetcher *)requestFetcher - receiptFetcher:(RCReceiptFetcher *)receiptFetcher - attributionFetcher:(RCAttributionFetcher *)attributionFetcher - attributionPoster:(RCAttributionPoster *)attributionPoster - backend:(RCBackend *)backend - storeKitWrapper:(RCStoreKitWrapper *)storeKitWrapper - notificationCenter:(NSNotificationCenter *)notificationCenter - systemInfo:(RCSystemInfo *)systemInfo - offeringsFactory:(RCOfferingsFactory *)offeringsFactory - deviceCache:(RCDeviceCache *)deviceCache - identityManager:(RCIdentityManager *)identityManager - subscriberAttributesManager:(RCSubscriberAttributesManager *)subscriberAttributesManager - operationDispatcher:(RCOperationDispatcher *)operationDispatcher - introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator - receiptParser:(RCReceiptParser *)receiptParser - purchaserInfoManager:(RCPurchaserInfoManager *)purchaserInfoManager - productsManager:(RCProductsManager *)productsManager - offeringsManager:(RCOfferingsManager *)offeringsManager - purchasesOrchestrator:(RCPurchasesOrchestrator *)purchasesOrchestrator; - -+ (void)setDefaultInstance:(nullable RCPurchases *)instance; - -- (void)_setPushTokenString:(nullable NSString *)pushToken; - -@property (nonatomic) RCDeviceCache *deviceCache; -@property (nonatomic) RCBackend *backend; -@property (nonatomic) NSNotificationCenter *notificationCenter; - -@end - - -NS_ASSUME_NONNULL_END diff --git a/Purchases/Public/Purchases.h b/Purchases/Public/Purchases.h index 92bafc38d7..db1957f777 100644 --- a/Purchases/Public/Purchases.h +++ b/Purchases/Public/Purchases.h @@ -16,5 +16,3 @@ FOUNDATION_EXPORT double PurchasesVersionNumber; Purchases version string */ FOUNDATION_EXPORT const unsigned char PurchasesVersionString[]; - -#import "RCPurchases.h" diff --git a/Purchases/Public/RCPurchases.h b/Purchases/Public/RCPurchases.h deleted file mode 100644 index 492292534e..0000000000 --- a/Purchases/Public/RCPurchases.h +++ /dev/null @@ -1,627 +0,0 @@ -// -// RCPurchases.h -// Purchases -// -// Created by RevenueCat. -// Copyright © 2019 RevenueCat. All rights reserved. -// - -@import PurchasesCoreSwift; - -#import - -@class SKProduct, SKPayment, SKPaymentTransaction, SKPaymentDiscount, SKProductDiscount, RCPurchaserInfo, - RCIntroEligibility, RCOfferings, RCOffering, RCPackage, RCSubscriberAttribute, RCSubscriberAttributesManager, - RCOperationDispatcher; -@protocol RCPurchasesDelegate; - -NS_ASSUME_NONNULL_BEGIN - -/** - Completion block for calls that send back a `PurchaserInfo` - */ -typedef void (^RCReceivePurchaserInfoBlock)(RCPurchaserInfo * _Nullable, NSError * _Nullable) NS_SWIFT_NAME(Purchases.ReceivePurchaserInfoBlock); - -/** - Completion block for `-[RCPurchases checkTrialOrIntroductoryPriceEligibility:completionBlock:]` - */ -typedef void (^RCReceiveIntroEligibilityBlock)(NSDictionary *) NS_SWIFT_NAME(Purchases.ReceiveIntroEligibilityBlock); - -/** - Completion block for `-[RCPurchases offeringsWithCompletionBlock:]` - */ -typedef void (^RCReceiveOfferingsBlock)(RCOfferings * _Nullable, NSError * _Nullable) NS_SWIFT_NAME(Purchases.ReceiveOfferingsBlock); - -/** - Completion block for `-[RCPurchases productsWithIdentifiers:completionBlock:]` - */ -typedef void (^RCReceiveProductsBlock)(NSArray *) NS_SWIFT_NAME(Purchases.ReceiveProductsBlock); - -/** - Completion block for `-[RCPurchases purchaseProduct:withCompletionBlock:]` - */ -typedef void (^RCPurchaseCompletedBlock)(SKPaymentTransaction * _Nullable, RCPurchaserInfo * _Nullable, NSError * _Nullable, BOOL userCancelled) NS_SWIFT_NAME(Purchases.PurchaseCompletedBlock); - -/** - Deferred block for `purchases:shouldPurchasePromoProduct:defermentBlock:` - */ -typedef void (^RCDeferredPromotionalPurchaseBlock)(RCPurchaseCompletedBlock); - -/** - * Deferred block for `-[RCPurchases paymentDiscountForProductDiscount:product:completion:]` - */ -API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)) -typedef void (^RCPaymentDiscountBlock)(SKPaymentDiscount * _Nullable, NSError * _Nullable) NS_SWIFT_NAME(Purchases.PaymentDiscountBlock); - -/** - `RCPurchases` is the entry point for Purchases.framework. It should be instantiated as soon as your app has a unique user id for your user. This can be when a user logs in if you have accounts or on launch if you can generate a random user identifier. - - @warning Only one instance of RCPurchases should be instantiated at a time! Use a configure method to let the framework handle the singleton instance for you. - */ -NS_SWIFT_NAME(Purchases) -@interface RCPurchases : NSObject - -// TODO during migration make these protected -@property (nonatomic) RCSubscriberAttributesManager *subscriberAttributesManager; -@property (nonatomic) RCOperationDispatcher *operationDispatcher; - -/** - Enable automatic collection of Apple Search Ads attribution. Disabled by default - */ -@property (class, nonatomic, assign) BOOL automaticAppleSearchAdsAttributionCollection; - -/** - Enable debug logging. Useful for debugging issues with the lovely team @RevenueCat -*/ -@property (class, nonatomic, assign) BOOL debugLogsEnabled DEPRECATED_MSG_ATTRIBUTE("use logLevel instead"); - -/** - Set a custom log handler for redirecting logs to your own logging system. - - By default, this sends Info, Warn, and Error messages. If you wish to receive Debug level messages, you must enable debug logs. - */ -+ (void)setLogHandler:(void(^)(RCLogLevel, NSString * _Nonnull))logHandler; - -/** - Used to set the log level. Useful for debugging issues with the lovely team @RevenueCat -*/ -@property (class, nonatomic) RCLogLevel logLevel; - -/** - Set this property to your proxy URL before configuring Purchases *only* if you've received a proxy key value from your RevenueCat contact. -*/ -@property (class, nonatomic, copy, nullable) NSURL *proxyURL; - -/** - Set this property to true *only* if you're transitioning an existing Mac app from the Legacy Mac App Store - into the Universal Store, and you've configured your RevenueCat app accordingly. Contact support before using this. -*/ -@property (class, nonatomic, assign) BOOL forceUniversalAppStore; - -/** - Set this property to true *only* when testing the ask-to-buy / SCA purchases flow. More information: - http://errors.rev.cat/ask-to-buy - */ -@property (class, nonatomic, assign) BOOL simulatesAskToBuyInSandbox API_AVAILABLE(ios(8.0), macos(10.14), watchos(6.2), macCatalyst(13.0), tvos(9.0)); - -/** - Configures an instance of the Purchases SDK with a specified API key. The instance will be set as a singleton. You should access the singleton instance using [RCPurchases sharedPurchases] - - @note Use this initializer if your app does not have an account system. `RCPurchases` will generate a unique identifier for the current device - and persist it to `NSUserDefaults`. This also affects the behavior of `restoreTransactionsWithCompletionBlock`. - - @param APIKey The API Key generated for your app from https://app.revenuecat.com/ - - @return An instantiated `RCPurchases` object that has been set as a singleton. - */ -+ (instancetype)configureWithAPIKey:(NSString *)APIKey; - -/** - Configures an instance of the Purchases SDK with a specified API key and app user ID. The instance will be set as a singleton. You should access the singleton instance using [RCPurchases sharedPurchases] - - @note Best practice is to use a salted hash of your unique app user ids. - - @warning Use this initializer if you have your own user identifiers that you manage. - - @param APIKey The API Key generated for your app from https://app.revenuecat.com/ - - @param appUserID The unique app user id for this user. This user id will allow users to share their purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. - - @return An instantiated `RCPurchases` object that has been set as a singleton. - */ -+ (instancetype)configureWithAPIKey:(NSString *)APIKey appUserID:(nullable NSString *)appUserID; - -/** - Configures an instance of the Purchases SDK with a custom userDefaults. Use this constructor if you want to sync status across a shared container, such as between a host app and an extension. The instance of the Purchases SDK will be set as a singleton. You should access the singleton instance using [RCPurchases sharedPurchases] - - @param APIKey The API Key generated for your app from https://app.revenuecat.com/ - - @param appUserID The unique app user id for this user. This user id will allow users to share their purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. - - @param observerMode Set this to TRUE if you have your own IAP implementation and want to use only RevenueCat's backend. Default is FALSE. - - @return An instantiated `RCPurchases` object that has been set as a singleton. - */ -+ (instancetype)configureWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - observerMode:(BOOL)observerMode; - -/** - Configures an instance of the Purchases SDK with a custom userDefaults. Use this constructor if you want to sync status across a shared container, such as between a host app and an extension. The instance of the Purchases SDK will be set as a singleton. You should access the singleton instance using [RCPurchases sharedPurchases] - - @param APIKey The API Key generated for your app from https://app.revenuecat.com/ - - @param appUserID The unique app user id for this user. This user id will allow users to share their purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. - - @param observerMode Set this to TRUE if you have your own IAP implementation and want to use only RevenueCat's backend. Default is FALSE. - - @param userDefaults Custom userDefaults to use - - @return An instantiated `RCPurchases` object that has been set as a singleton. - */ -+ (instancetype)configureWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - observerMode:(BOOL)observerMode - userDefaults:(nullable NSUserDefaults *)userDefaults; - -/** - Indicates whether the user is allowed to make payments. - */ -+ (BOOL)canMakePayments; - -/** - @return A singleton `RCPurchases` object. Call this after a configure method to access the singleton. - @note: If the SDK has not been configured, calls to sharedPurchases will raise an exception. Make sure to configure the SDK before making calls to sharedPurchases. - */ -@property (class, nonatomic, readonly) RCPurchases *sharedPurchases; - -#pragma mark Configuration - -/** - @note True if the SDK has been configured, false otherwise. This property should only be used in special circumstances. If the shared instance has not been configured, - calls made to it will raise an exception. - */ -@property (class, nonatomic, readonly) BOOL isConfigured; - -/** Set this to true if you are passing in an appUserID but it is anonymous, this is true by default if you didn't pass an appUserID - If a user tries to purchase a product that is active on the current app store account, we will treat it as a restore and alias - the new ID with the previous id. - See https://docs.revenuecat.com/docs/user-ids - */ -@property (nonatomic) BOOL allowSharingAppStoreAccount - __attribute((deprecated("Configure behavior through the RevenueCat dashboard instead."))); - -/// Default to YES, set this to NO if you are finishing transactions with your own StoreKit queue listener -@property (nonatomic) BOOL finishTransactions; - -/// This version of the Purchases framework -+ (NSString *)frameworkVersion; - -/// Delegate for `RCPurchases` instance. The delegate is responsible for handling promotional product purchases and changes to purchaser information. -@property (nonatomic, weak, nullable) id delegate; - -#pragma mark Identity - -/// The `appUserID` used by `RCPurchases`. If not passed on initialization this will be generated and cached by `RCPurchases`. -@property (nonatomic, readonly) NSString *appUserID; - -/// If the `appUserID` has been generated by RevenueCat -@property (nonatomic, readonly) BOOL isAnonymous; - -/** - This function will alias two appUserIDs together. - @param alias The new appUserID that should be linked to the currently identified appUserID - @param completion An optional completion block called when the aliasing has been successful. This completion block will receive an error if there's been one. - */ -- (void)createAlias:(NSString *)alias completionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(createAlias(_:_:)) __attribute((deprecated("Use logIn instead."))); - -/** - This function will identify the current user with an appUserID. Typically this would be used after a logout to identify a new user without calling configure. - @param appUserID The appUserID that should be linked to the current user. - */ -- (void)identify:(NSString *)appUserID completionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(identify(_:_:)) __attribute((deprecated("Use logIn instead."))); - -/** - * Resets the Purchases client clearing the saved appUserID. This will generate a random user id and save it in the cache. - */ -- (void)resetWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(reset(_:)) __attribute((deprecated("Use logOut instead."))); - -/** - This function will logIn the current user with an appUserID. - @param appUserID The appUserID that should be linked to the current user. - The callback will be called with the latest PurchaserInfo for the user, as well as a boolean indicating whether the user was created for the first - time in the RevenueCat backend. - See https://docs.revenuecat.com/docs/user-ids - */ -- (void) logIn:(NSString *)appUserID -completionBlock:(void (^)(RCPurchaserInfo * _Nullable purchaserInfo, BOOL created, NSError * _Nullable error))completion -NS_SWIFT_NAME(logIn(_:_:)); - -/** - Logs out the Purchases client clearing the saved appUserID. This will generate a random user id and save it in the cache. - If this method is called and the current user is anonymous, it will return an error. - See https://docs.revenuecat.com/docs/user-ids - */ -- (void)logOutWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(logOut(_:)); - -#pragma mark Attribution - -/** - Send your attribution data to RevenueCat so you can track the revenue generated by your different campaigns. - - @param data Dictionary provided by the network. See https://docs.revenuecat.com/docs/attribution - @param network Enum for the network the data is coming from, see `RCAttributionNetwork` for supported networks - */ -+ (void)addAttributionData:(NSDictionary *)data - fromNetwork:(RCAttributionNetwork)network __attribute((deprecated("Use the set functions instead."))); - -/** - Send your attribution data to RevenueCat so you can track the revenue generated by your different campaigns. - - @param data Dictionary provided by the network. See https://docs.revenuecat.com/docs/attribution - @param network Enum for the network the data is coming from, see `RCAttributionNetwork` for supported networks - @param networkUserId User Id that should be sent to the network. Default is the current App User Id - */ -+ (void)addAttributionData:(NSDictionary *)data - fromNetwork:(RCAttributionNetwork)network - forNetworkUserId:(nullable NSString *)networkUserId NS_SWIFT_NAME(addAttributionData(_:from:forNetworkUserId:)) -__attribute((deprecated("Use the set functions instead."))); - -#pragma mark Purchases - -/** - Get latest available purchaser info. - - @param completion A completion block called when purchaser info is available and not stale. Called immediately if purchaser info is cached. Purchaser info can be nil if an error occurred. - */ -- (void)purchaserInfoWithCompletionBlock:(RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(purchaserInfo(_:)); - -/** - Fetch the configured offerings for this users. Offerings allows you to configure your in-app products via RevenueCat and greatly simplifies management. See the guide (https://docs.revenuecat.com/entitlements) for more info. - - Offerings will be fetched and cached on instantiation so that, by the time they are needed, your prices are loaded for your purchase flow. Time is money. - - @param completion A completion block called when offerings are available. Called immediately if offerings are cached. Offerings will be nil if an error occurred. - */ -- (void)offeringsWithCompletionBlock:(RCReceiveOfferingsBlock)completion NS_SWIFT_NAME(offerings(_:)); - -/** - Fetches the `SKProducts` for your IAPs for given `productIdentifiers`. Use this method if you aren't using `-offeringsWithCompletionBlock:`. - You should use offerings though. - - @note `completion` may be called without `SKProduct`s that you are expecting. This is usually caused by iTunesConnect configuration errors. Ensure your IAPs have the "Ready to Submit" status in iTunesConnect. Also ensure that you have an active developer program subscription and you have signed the latest paid application agreements. If you're having trouble see: https://www.revenuecat.com/2018/10/11/configuring-in-app-products-is-hard - - @param productIdentifiers A set of product identifiers for in app purchases setup via iTunesConnect. This should be either hard coded in your application, from a file, or from a custom endpoint if you want to be able to deploy new IAPs without an app update. - @param completion An @escaping callback that is called with the loaded products. If the fetch fails for any reason it will return an empty array. - */ -- (void)productsWithIdentifiers:(NSArray *)productIdentifiers - completionBlock:(RCReceiveProductsBlock)completion -NS_SWIFT_NAME(products(_:_:)); - -/** - Use this function if you are not using the Offerings system to purchase an `SKProduct`. If you are using the Offerings system, use `-[RCPurchases purchasePackage:withCompletionBlock]` instead. - - Call this method when a user has decided to purchase a product. Only call this in direct response to user input. - - From here `Purchases` will handle the purchase with `StoreKit` and call the `RCPurchaseCompletedBlock`. - - @note You do not need to finish the transaction yourself in the completion callback, Purchases will handle this for you. - - @param product The `SKProduct` the user intends to purchase - - @param completion A completion block that is called when the purchase completes. If the purchase was successful there will be a `SKPaymentTransaction` and a `RCPurchaserInfo`. If the purchase was not successful, there will be an `NSError`. If the user cancelled, `userCancelled` will be `YES`. - */ -- (void)purchaseProduct:(SKProduct *)product withCompletionBlock:(RCPurchaseCompletedBlock)completion -NS_SWIFT_NAME(purchaseProduct(_:_:)); - -/** - Purchase the passed `RCPackage`. - - Call this method when a user has decided to purchase a product. Only call this in direct response to user input. - - From here `Purchases` will handle the purchase with `StoreKit` and call the `RCPurchaseCompletedBlock`. - - @note You do not need to finish the transaction yourself in the completion callback, Purchases will handle this for you. - - @param package The `RCPackage` the user intends to purchase - - @param completion A completion block that is called when the purchase completes. If the purchase was successful there will be a `SKPaymentTransaction` and a `RCPurchaserInfo`. If the purchase was not successful, there will be an `NSError`. If the user cancelled, `userCancelled` will be `YES`. - */ -- (void)purchasePackage:(RCPackage *)package withCompletionBlock:(RCPurchaseCompletedBlock)completion -NS_SWIFT_NAME(purchasePackage(_:_:)); - -/** - This method will post all purchases associated with the current App Store account to RevenueCat and become associated with the current `appUserID`. - If the receipt is being used by an existing user, the current `appUserID` will be aliased together with the `appUserID` of the existing user. - Going forward, either `appUserID` will be able to reference the same user. - - You shouldn't use this method if you have your own account system. In that case "restoration" is provided by your app passing - the same `appUserId` used to purchase originally. - - @note This may force your users to enter the App Store password so should only be performed on request of the user. - Typically with a button in settings or near your purchase UI. Use syncPurchasesWithCompletionBlock - if you need to restore transactions programmatically. - */ -- (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(restoreTransactions(_:)); - -/** - This method will post all purchases associated with the current App Store account to RevenueCat and become associated with the current `appUserID`. - If the receipt is being used by an existing user, the current `appUserID` will be aliased together with the `appUserID` of the existing user. - Going forward, either `appUserID` will be able to reference the same user. - - @warning This function should only be called if you're not calling any purchase method. - - @note This method will not trigger a login prompt from App Store. However, if the receipt currently on the device does not contain subscriptions, - but the user has made subscription purchases, this method won't be able to restore them. Use restoreTransactionsWithCompletionBlock to cover those cases. - */ -- (void)syncPurchasesWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion -NS_SWIFT_NAME(syncPurchases(_:)); - - -/** - Computes whether or not a user is eligible for the introductory pricing period of a given product. You should use this method to determine whether or not you show the user the normal product price or the introductory price. This also applies to trials (trials are considered a type of introductory pricing). - - @note Subscription groups are automatically collected for determining eligibility. If RevenueCat can't definitively compute the eligibilty, most likely because of missing group information, it will return `RCIntroEligibilityStatusUnknown`. The best course of action on unknown status is to display the non-intro pricing, to not create a misleading situation. To avoid this, make sure you are testing with the latest version of iOS so that the subscription group can be collected by the SDK. - - @param productIdentifiers Array of product identifiers for which you want to compute eligibility - @param receiveEligibility A block that receives a dictionary of product_id -> `RCIntroEligibility`. -*/ -- (void)checkTrialOrIntroductoryPriceEligibility:(NSArray *)productIdentifiers - completionBlock:(RCReceiveIntroEligibilityBlock)receiveEligibility; - -/** - Use this function to retrieve the `SKPaymentDiscount` for a given `SKProduct`. - - @param discount The `SKProductDiscount` to apply to the product. - - @param product The `SKProduct` the user intends to purchase. - - @param completion A completion block that is called when the `SKPaymentDiscount` is returned. If it was not successful, there will be an `NSError`. -*/ -- (void)paymentDiscountForProductDiscount:(SKProductDiscount *)discount - product:(SKProduct *)product - completion:(RCPaymentDiscountBlock)completion API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)); - - -/** - Use this function if you are not using the Offerings system to purchase an `SKProduct` with an applied `SKPaymentDiscount`. If you are using the Offerings system, use `-[RCPurchases purchasePackage:withDiscount:withCompletionBlock]` instead. - - Call this method when a user has decided to purchase a product with an applied discount. Only call this in direct response to user input. - - From here `Purchases` will handle the purchase with `StoreKit` and call the `RCPurchaseCompletedBlock`. - - @note You do not need to finish the transaction yourself in the completion callback, Purchases will handle this for you. - - @param product The `SKProduct` the user intends to purchase - - @param discount The `SKPaymentDiscount` to apply to the purchase - - @param completion A completion block that is called when the purchase completes. If the purchase was successful there will be a `SKPaymentTransaction` and a `RCPurchaserInfo`. If the purchase was not successful, there will be an `NSError`. If the user cancelled, `userCancelled` will be `YES`. - */ -- (void)purchaseProduct:(SKProduct *)product - withDiscount:(SKPaymentDiscount *)discount - completionBlock:(RCPurchaseCompletedBlock)completion NS_SWIFT_NAME(purchaseProduct(_:discount:_:)) API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)); - -/** - Purchase the passed `RCPackage`. - - Call this method when a user has decided to purchase a product with an applied discount. Only call this in direct response to user input. - - From here `Purchases` will handle the purchase with `StoreKit` and call the `RCPurchaseCompletedBlock`. - - @note You do not need to finish the transaction yourself in the completion callback, Purchases will handle this for you. - - @param package The `RCPackage` the user intends to purchase - - @param discount The `SKPaymentDiscount` to apply to the purchase - - @param completion A completion block that is called when the purchase completes. If the purchase was successful there will be a `SKPaymentTransaction` and a `RCPurchaserInfo`. If the purchase was not successful, there will be an `NSError`. If the user cancelled, `userCancelled` will be `YES`. - */ -- (void)purchasePackage:(RCPackage *)package - withDiscount:(SKPaymentDiscount *)discount - completionBlock:(RCPurchaseCompletedBlock)completion NS_SWIFT_NAME(purchasePackage(_:discount:_:)) API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)); - - -/** - Invalidates the cache for purchaser information. - - Most apps will not need to use this method; invalidating the cache can leave your app in an invalid state. - Refer to https://docs.revenuecat.com/docs/purchaserinfo#section-get-user-information for more information on - using the cache properly. - - This is useful for cases where purchaser information might have been updated outside of the app, like if a - promotional subscription is granted through the RevenueCat dashboard. - */ -- (void)invalidatePurchaserInfoCache; - -/** - Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect. - */ -- (void)presentCodeRedemptionSheet API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(tvos, macos, watchos); - - -#pragma mark Subscriber Attributes - -/** - Subscriber attributes are useful for storing additional, structured information on a user. - Since attributes are writable using a public key they should not be used for - managing secure or sensitive information such as subscription status, coins, etc. - - Key names starting with "$" are reserved names used by RevenueCat. For a full list of key - restrictions refer to our guide: https://docs.revenuecat.com/docs/subscriber-attributes - - @param attributes Map of attributes by key. Set the value as an empty string to delete an attribute. -*/ -- (void)setAttributes:(NSDictionary *)attributes; - -/** - * Subscriber attribute associated with the email address for the user - * - * @param email Empty String or nil will delete the subscriber attribute. - */ -- (void)setEmail:(nullable NSString *)email; - -/** - * Subscriber attribute associated with the phone number for the user - * - * @param phoneNumber Empty String or nil will delete the subscriber attribute. - */ -- (void)setPhoneNumber:(nullable NSString *)phoneNumber; - -/** - * Subscriber attribute associated with the display name for the user - * - * @param displayName Empty String or nil will delete the subscriber attribute. - */ -- (void)setDisplayName:(nullable NSString *)displayName; - -/** - * Subscriber attribute associated with the push token for the user - * - * @param pushToken nil will delete the subscriber attribute. - */ -- (void)setPushToken:(nullable NSData *)pushToken; - -/** - * Subscriber attribute associated with the Adjust Id for the user - * Required for the RevenueCat Adjust integration - * - * @param adjustID nil will delete the subscriber attribute - */ -- (void)setAdjustID:(nullable NSString *)adjustID; - -/** - * Subscriber attribute associated with the Appsflyer Id for the user - * Required for the RevenueCat Appsflyer integration - * - * @param appsflyerID nil will delete the subscriber attribute - */ -- (void)setAppsflyerID:(nullable NSString *)appsflyerID; - -/** - * Subscriber attribute associated with the Facebook SDK Anonymous Id for the user - * Recommended for the RevenueCat Facebook integration - * - * @param fbAnonymousID nil will delete the subscriber attribute - */ -- (void)setFBAnonymousID:(nullable NSString *)fbAnonymousID; - -/** - * Subscriber attribute associated with the mParticle Id for the user - * Recommended for the RevenueCat mParticle integration - * - * @param mparticleID nil will delete the subscriber attribute - */ -- (void)setMparticleID:(nullable NSString *)mparticleID; - -/** - * Subscriber attribute associated with the OneSignal Player Id for the user - * Required for the RevenueCat OneSignal integration - * - * @param onesignalID nil will delete the subscriber attribute - */ -- (void)setOnesignalID:(nullable NSString *)onesignalID; - -/** - * Subscriber attribute associated with the install media source for the user - * - * @param mediaSource nil will delete the subscriber attribute. - */ -- (void)setMediaSource:(nullable NSString *)mediaSource; - -/** - * Subscriber attribute associated with the install campaign for the user - * - * @param campaign nil will delete the subscriber attribute. - */ -- (void)setCampaign:(nullable NSString *)campaign; - -/** - * Subscriber attribute associated with the install ad group for the user - * - * @param adGroup nil will delete the subscriber attribute. - */ -- (void)setAdGroup:(nullable NSString *)adGroup; - -/** - * Subscriber attribute associated with the install ad for the user - * - * @param ad nil will delete the subscriber attribute. - */ -- (void)setAd:(nullable NSString *)ad; - -/** - * Subscriber attribute associated with the install keyword for the user - * - * @param keyword nil will delete the subscriber attribute. - */ -- (void)setKeyword:(nullable NSString *)keyword; - -/** - * Subscriber attribute associated with the install ad creative for the user - * - * @param creative nil will delete the subscriber attribute. - */ -- (void)setCreative:(nullable NSString *)creative; - -/** - * Automatically collect subscriber attributes associated with the device identifiers - * $idfa, $idfv, $ip - * - */ -- (void)collectDeviceIdentifiers; - -#pragma mark Unavailable Methods -#define RC_UNAVAILABLE(msg) __attribute__((unavailable(msg))); -/// :nodoc: -typedef void (^RCReceiveEntitlementsBlock)(id _Nullable, NSError * _Nullable) NS_SWIFT_NAME(Purchases.ReceiveEntitlementsBlock); -/// :nodoc: -- (void)makePurchase:(SKProduct *)product withCompletionBlock:(RCPurchaseCompletedBlock)block -NS_SWIFT_NAME(makePurchaseSwift(_:_:)) RC_UNAVAILABLE("makePurchase: has been replaced by purchaseProduct:"); -/// :nodoc: -- (void)entitlementsWithCompletionBlock:(RCReceiveEntitlementsBlock)completion -NS_SWIFT_NAME(entitlements(_:)) RC_UNAVAILABLE("entitlements: has been replaced with offerings:. See https://docs.revenuecat.com/docs/offerings-migration"); -/// :nodoc: -- (void)makePurchase:(SKProduct *)product - withDiscount:(nullable SKPaymentDiscount *)discount - completionBlock:(RCPurchaseCompletedBlock)completion NS_SWIFT_NAME(makePurchase(_:discount:_:)) API_AVAILABLE(ios(12.2), macosx(10.14.4)) __attribute__((unavailable("makePurchase:withDiscount: has been replaced by purchaseProduct:withDiscount:")));; - -#undef RC_UNAVAILABLE - -@end - -/** - Delegate for `RCPurchases` responsible for handling updating your app's state in response to updated purchaser info or promotional product purchases. - - @note Delegate methods can be called at any time after the `delegate` is set, not just in response to `purchaserInfo:` calls. Ensure your app is capable of handling these calls at anytime if `delegate` is set. - */ -NS_SWIFT_NAME(PurchasesDelegate) -@protocol RCPurchasesDelegate -@optional - -/** - Called whenever `RCPurchases` receives updated purchaser info. This may happen periodically - throughout the life of the app if new information becomes available (e.g. UIApplicationDidBecomeActive). - - @param purchases Related `RCPurchases` object - @param purchaserInfo Updated `RCPurchaserInfo` - */ -- (void)purchases:(RCPurchases *)purchases didReceiveUpdatedPurchaserInfo:(RCPurchaserInfo *)purchaserInfo -NS_SWIFT_NAME(purchases(_:didReceiveUpdated:)); - -/** - Called when a user initiates a promotional in-app purchase from the App Store. If your app is able to handle a purchase at the current time, run the deferment block in this method. If the app is not in a state to make a purchase: cache the defermentBlock, then call the defermentBlock when the app is ready to make the promotional purchase. If the purchase should never be made, you don't need to ever call the defermentBlock and `RCPurchases` will not proceed with promotional purchases. - - @param product `SKProduct` the product that was selected from the app store - */ -- (void)purchases:(RCPurchases *)purchases shouldPurchasePromoProduct:(SKProduct *)product defermentBlock:(RCDeferredPromotionalPurchaseBlock)makeDeferredPurchase; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m deleted file mode 100644 index c0f9806ad7..0000000000 --- a/Purchases/Public/RCPurchases.m +++ /dev/null @@ -1,799 +0,0 @@ -// -// RCPurchases.m -// Purchases -// -// Created by RevenueCat. -// Copyright © 2019 RevenueCat. All rights reserved. -// - -@import PurchasesCoreSwift; - -#import "RCPurchases+Protected.h" -#import "RCPurchases.h" - -@interface RCPurchases () - -/** - * Completion block for calls that send back receipt data - */ -typedef void (^RCReceiveReceiptDataBlock)(NSData *); - -typedef NSDictionary *RCSubscriberAttributeDict; - -@property (nonatomic) RCStoreKitRequestFetcher *requestFetcher; -@property (nonatomic) RCProductsManager *productsManager; -@property (nonatomic) RCReceiptFetcher *receiptFetcher; -@property (nonatomic) RCBackend *backend; -@property (nonatomic) RCStoreKitWrapper *storeKitWrapper; -@property (nonatomic) NSNotificationCenter *notificationCenter; -@property (nonatomic) RCAttributionFetcher *attributionFetcher; -@property (nonatomic) RCAttributionPoster *attributionPoster; -@property (nonatomic) RCOfferingsFactory *offeringsFactory; -@property (nonatomic) RCDeviceCache *deviceCache; -@property (nonatomic) RCIdentityManager *identityManager; -@property (nonatomic) RCSystemInfo *systemInfo; -@property (nonatomic) RCIntroEligibilityCalculator *introEligibilityCalculator; -@property (nonatomic) RCReceiptParser *receiptParser; -@property (nonatomic) RCPurchaserInfoManager *purchaserInfoManager; -@property (nonatomic) RCOfferingsManager *offeringsManager; -@property (nonatomic) RCPurchasesOrchestrator *purchasesOrchestrator; - -@end - -static RCPurchases *_sharedPurchases = nil; - -@implementation RCPurchases - -#pragma mark - Configuration - -- (BOOL)allowSharingAppStoreAccount { - return self.purchasesOrchestrator.allowSharingAppStoreAccount; -} - -- (void)setAllowSharingAppStoreAccount:(BOOL)allow { - self.purchasesOrchestrator.allowSharingAppStoreAccount = allow; -} - -static BOOL _automaticAppleSearchAdsAttributionCollection = NO; - -+ (void)setAutomaticAppleSearchAdsAttributionCollection:(BOOL)automaticAppleSearchAdsAttributionCollection { - _automaticAppleSearchAdsAttributionCollection = automaticAppleSearchAdsAttributionCollection; -} - -+ (BOOL)automaticAppleSearchAdsAttributionCollection { - return _automaticAppleSearchAdsAttributionCollection; -} - -+ (void)setDebugLogsEnabled:(BOOL)enabled { - RCLogLevel level = enabled ? RCLogLevelDebug : RCLogLevelInfo; - [self setLogLevel:level]; -} - -+ (BOOL)debugLogsEnabled { - return self.logLevel <= RCLogLevelDebug; -} - -+ (void)setLogHandler:(void(^)(RCLogLevel, NSString * _Nonnull))logHandler { - RCLog.logHandler = logHandler; -} - -+ (RCLogLevel)logLevel { - return RCLog.logLevel; -} - -+ (void)setLogLevel:(RCLogLevel)logLevel { - RCLog.logLevel = logLevel; -} - -+ (NSURL *)proxyURL { - return RCSystemInfo.proxyURL; -} - -+ (void)setProxyURL:(nullable NSURL *)proxyURL { - RCSystemInfo.proxyURL = proxyURL; -} - -+ (BOOL)forceUniversalAppStore { - return RCSystemInfo.forceUniversalAppStore; -} - -+ (void)setForceUniversalAppStore:(BOOL)forceUniversalAppStore { - RCSystemInfo.forceUniversalAppStore = forceUniversalAppStore; -} - -+ (BOOL)simulatesAskToBuyInSandbox { - return RCStoreKitWrapper.simulatesAskToBuyInSandbox; -} - -+ (void)setSimulatesAskToBuyInSandbox:(BOOL)simulatesAskToBuyInSandbox { - RCStoreKitWrapper.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox; -} - -+ (NSString *)frameworkVersion { - return RCSystemInfo.frameworkVersion; -} - -- (BOOL)finishTransactions { - return self.systemInfo.finishTransactions; -} - -- (void)setFinishTransactions:(BOOL)finishTransactions { - self.systemInfo.finishTransactions = finishTransactions; -} - -+ (BOOL)isConfigured { - return _sharedPurchases != nil; -} - -+ (instancetype)sharedPurchases { - NSCAssert(_sharedPurchases, RCStrings.configure.no_singleton_instance); - return _sharedPurchases; -} - -+ (void)setDefaultInstance:(RCPurchases *)instance { - @synchronized([RCPurchases class]) { - if (_sharedPurchases) { - [RCLog info:[NSString stringWithFormat:@"%@", RCStrings.configure.purchase_instance_already_set]]; - } - _sharedPurchases = instance; - } -} - -+ (BOOL)canMakePayments { - return [SKPaymentQueue canMakePayments]; -} - -+ (instancetype)configureWithAPIKey:(NSString *)APIKey { - return [self configureWithAPIKey:APIKey appUserID:nil]; -} - -+ (instancetype)configureWithAPIKey:(NSString *)APIKey appUserID:(nullable NSString *)appUserID { - return [self configureWithAPIKey:APIKey appUserID:appUserID observerMode:false]; -} - -+ (instancetype)configureWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - observerMode:(BOOL)observerMode { - return [self configureWithAPIKey:APIKey appUserID:appUserID observerMode:observerMode userDefaults:nil]; -} - -+ (instancetype)configureWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - observerMode:(BOOL)observerMode - userDefaults:(nullable NSUserDefaults *)userDefaults { - return [self configureWithAPIKey:APIKey - appUserID:appUserID - observerMode:observerMode - userDefaults:userDefaults - platformFlavor:nil - platformFlavorVersion:nil]; -} - -+ (instancetype)configureWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - observerMode:(BOOL)observerMode - userDefaults:(nullable NSUserDefaults *)userDefaults - platformFlavor:(NSString *)platformFlavor - platformFlavorVersion:(NSString *)platformFlavorVersion { - RCPurchases *purchases = [[self alloc] initWithAPIKey:APIKey - appUserID:appUserID - userDefaults:userDefaults - observerMode:observerMode - platformFlavor:platformFlavor - platformFlavorVersion:platformFlavorVersion]; - [self setDefaultInstance:purchases]; - return purchases; -} - -- (instancetype)initWithAPIKey:(NSString *)APIKey appUserID:(nullable NSString *)appUserID { - return [self initWithAPIKey:APIKey - appUserID:appUserID - userDefaults:nil - observerMode:false - platformFlavor:nil - platformFlavorVersion:nil]; -} - -// TODO: if possible, move to new DI manager class -- (instancetype)initWithAPIKey:(NSString *)APIKey - appUserID:(nullable NSString *)appUserID - userDefaults:(nullable NSUserDefaults *)userDefaults - observerMode:(BOOL)observerMode - platformFlavor:(nullable NSString *)platformFlavor - platformFlavorVersion:(nullable NSString *)platformFlavorVersion { - RCOperationDispatcher *operationDispatcher = [[RCOperationDispatcher alloc] init]; - RCReceiptRefreshRequestFactory *receiptRefreshRequestFactory = [[RCReceiptRefreshRequestFactory alloc] init]; - RCStoreKitRequestFetcher *fetcher = [[RCStoreKitRequestFetcher alloc] - initWithRequestFactory:receiptRefreshRequestFactory - operationDispatcher:operationDispatcher]; - RCReceiptFetcher *receiptFetcher = [[RCReceiptFetcher alloc] initWithRequestFetcher:fetcher]; - NSError *error = nil; - RCSystemInfo *systemInfo = [[RCSystemInfo alloc] initWithPlatformFlavor:platformFlavor - platformFlavorVersion:platformFlavorVersion - finishTransactions:!observerMode - error:&error]; - NSAssert(systemInfo, error.localizedDescription); - - RCETagManager *eTagManager = [[RCETagManager alloc] init]; - - RCBackend *backend = [[RCBackend alloc] initWithAPIKey:APIKey - systemInfo:systemInfo - eTagManager:eTagManager - operationDispatcher:operationDispatcher]; - RCStoreKitWrapper *storeKitWrapper = [[RCStoreKitWrapper alloc] init]; - RCOfferingsFactory *offeringsFactory = [[RCOfferingsFactory alloc] init]; - - if (userDefaults == nil) { - userDefaults = [NSUserDefaults standardUserDefaults]; - } - - RCDeviceCache *deviceCache = [[RCDeviceCache alloc] initWithUserDefaults:userDefaults]; - RCIntroEligibilityCalculator *introCalculator = [[RCIntroEligibilityCalculator alloc] init]; - RCReceiptParser *receiptParser = [[RCReceiptParser alloc] init]; - RCPurchaserInfoManager *purchaserInfoManager = [[RCPurchaserInfoManager alloc] - initWithOperationDispatcher:operationDispatcher - deviceCache:deviceCache - backend:backend - systemInfo:systemInfo]; - RCIdentityManager *identityManager = [[RCIdentityManager alloc] initWithDeviceCache:deviceCache - backend:backend - purchaserInfoManager:purchaserInfoManager]; - RCAttributionTypeFactory *attributionTypeFactory = [[RCAttributionTypeFactory alloc] init]; - RCAttributionFetcher *attributionFetcher = [[RCAttributionFetcher alloc] - initWithAttributionFactory:attributionTypeFactory - systemInfo:systemInfo]; - RCAttributionDataMigrator *attributionDataMigrator = [[RCAttributionDataMigrator alloc] init]; - RCSubscriberAttributesManager *subscriberAttributesManager = - [[RCSubscriberAttributesManager alloc] initWithBackend:backend - deviceCache:deviceCache - attributionFetcher:attributionFetcher - attributionDataMigrator:attributionDataMigrator]; - - RCAttributionPoster *attributionPoster = [[RCAttributionPoster alloc] initWithDeviceCache:deviceCache - identityManager:identityManager - backend:backend - attributionFetcher:attributionFetcher - subscriberAttributesManager:subscriberAttributesManager]; - - RCProductsRequestFactory *productsRequestFactory = [[RCProductsRequestFactory alloc] init]; - RCProductsManager *productsManager = [[RCProductsManager alloc] initWithProductsRequestFactory:productsRequestFactory]; - RCOfferingsManager *offeringsManager = [[RCOfferingsManager alloc] initWithDeviceCache:deviceCache - operationDispatcher:operationDispatcher - systemInfo:systemInfo - backend:backend - offeringsFactory:offeringsFactory - productsManager:productsManager]; - RCPurchasesOrchestrator *purchasesOrchestrator = [[RCPurchasesOrchestrator alloc] - initWithProductsManager:productsManager - storeKitWrapper:storeKitWrapper - systemInfo:systemInfo - subscriberAttributesManager:subscriberAttributesManager - operationDispatcher:operationDispatcher - receiptFetcher:receiptFetcher - purchaserInfoManager:purchaserInfoManager - backend:backend - identityManager:identityManager - receiptParser:receiptParser - deviceCache:deviceCache]; - purchasesOrchestrator.maybeDelegate = self; - return [self initWithAppUserID:appUserID - requestFetcher:fetcher - receiptFetcher:receiptFetcher - attributionFetcher:attributionFetcher - attributionPoster:attributionPoster - backend:backend - storeKitWrapper:storeKitWrapper - notificationCenter:[NSNotificationCenter defaultCenter] - systemInfo:systemInfo - offeringsFactory:offeringsFactory - deviceCache:deviceCache - identityManager:identityManager - subscriberAttributesManager:subscriberAttributesManager - operationDispatcher:operationDispatcher - introEligibilityCalculator:introCalculator - receiptParser:receiptParser - purchaserInfoManager:purchaserInfoManager - productsManager:productsManager - offeringsManager:offeringsManager - purchasesOrchestrator:purchasesOrchestrator]; -} - -- (instancetype)initWithAppUserID:(nullable NSString *)appUserID - requestFetcher:(RCStoreKitRequestFetcher *)requestFetcher - receiptFetcher:(RCReceiptFetcher *)receiptFetcher - attributionFetcher:(RCAttributionFetcher *)attributionFetcher - attributionPoster:(RCAttributionPoster *)attributionPoster - backend:(RCBackend *)backend - storeKitWrapper:(RCStoreKitWrapper *)storeKitWrapper - notificationCenter:(NSNotificationCenter *)notificationCenter - systemInfo:(RCSystemInfo *)systemInfo - offeringsFactory:(RCOfferingsFactory *)offeringsFactory - deviceCache:(RCDeviceCache *)deviceCache - identityManager:(RCIdentityManager *)identityManager - subscriberAttributesManager:(RCSubscriberAttributesManager *)subscriberAttributesManager - operationDispatcher:(RCOperationDispatcher *)operationDispatcher - introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator - receiptParser:(RCReceiptParser *)receiptParser - purchaserInfoManager:(RCPurchaserInfoManager *)purchaserInfoManager - productsManager:(RCProductsManager *)productsManager - offeringsManager:(RCOfferingsManager *)offeringsManager - purchasesOrchestrator:(RCPurchasesOrchestrator *)purchasesOrchestrator { - if (self = [super init]) { - [RCLog debug:[NSString stringWithFormat:@"%@", RCStrings.configure.debug_enabled]]; - [RCLog debug:[NSString stringWithFormat:RCStrings.configure.sdk_version, self.class.frameworkVersion]]; - [RCLog user:[NSString stringWithFormat:RCStrings.configure.initial_app_user_id, appUserID]]; - - self.requestFetcher = requestFetcher; - self.receiptFetcher = receiptFetcher; - self.attributionFetcher = attributionFetcher; - self.attributionPoster = attributionPoster; - self.backend = backend; - self.storeKitWrapper = storeKitWrapper; - self.offeringsFactory = offeringsFactory; - self.deviceCache = deviceCache; - self.identityManager = identityManager; - - self.notificationCenter = notificationCenter; - - self.systemInfo = systemInfo; - self.subscriberAttributesManager = subscriberAttributesManager; - self.operationDispatcher = operationDispatcher; - self.introEligibilityCalculator = introEligibilityCalculator; - self.receiptParser = receiptParser; - self.purchaserInfoManager = purchaserInfoManager; - self.productsManager = productsManager; - self.offeringsManager = offeringsManager; - self.purchasesOrchestrator = purchasesOrchestrator; - - [self.identityManager configureWithAppUserID:appUserID]; - - [self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isBackgrounded) { - if (!isBackgrounded) { - [self.operationDispatcher dispatchOnWorkerThreadWithRandomDelay:NO block:^{ - [self updateAllCachesWithCompletionBlock:nil]; - }]; - } else { - [self.purchaserInfoManager sendCachedPurchaserInfoIfAvailableForAppUserID:self.appUserID]; - } - }]; - self.storeKitWrapper.delegate = purchasesOrchestrator; - - [self subscribeToAppStateNotifications]; - - [self.attributionPoster postPostponedAttributionDataIfNeeded]; - [self postAppleSearchAddsAttributionCollectionIfNeeded]; - } - - return self; -} - -- (void)subscribeToAppStateNotifications { - [self.notificationCenter addObserver:self - selector:@selector(applicationDidBecomeActive:) - name:RCSystemInfo.applicationDidBecomeActiveNotification - object:nil]; - [self.notificationCenter addObserver:self - selector:@selector(applicationWillResignActive:) - name:RCSystemInfo.applicationWillResignActiveNotification - object:nil]; -} - -- (void)dealloc { - self.storeKitWrapper.delegate = nil; - self.purchaserInfoManager.delegate = nil; - [self.notificationCenter removeObserver:self]; - _delegate = nil; -} - -@synthesize delegate = _delegate; - -- (void)setDelegate:(id )delegate { - _delegate = delegate; - self.purchaserInfoManager.delegate = self; - [self.purchaserInfoManager sendCachedPurchaserInfoIfAvailableForAppUserID:self.appUserID]; - [RCLog debug:[NSString stringWithFormat:@"%@", RCStrings.configure.delegate_set]]; -} - -#pragma mark - Public Methods - -#pragma mark Attribution - -- (void)postAttributionData:(NSDictionary *)data - fromNetwork:(RCAttributionNetwork)network - forNetworkUserId:(nullable NSString *)networkUserId { - [self.attributionPoster postAttributionData:data - fromNetwork:network - forNetworkUserId:networkUserId]; -} - -+ (void)addAttributionData:(NSDictionary *)data - fromNetwork:(RCAttributionNetwork)network { - [self addAttributionData:data fromNetwork:network forNetworkUserId:nil]; -} - -+ (void)addAttributionData:(NSDictionary *)data - fromNetwork:(RCAttributionNetwork)network - forNetworkUserId:(nullable NSString *)networkUserId { - if (self.isConfigured) { - [_sharedPurchases postAttributionData:data fromNetwork:network forNetworkUserId:networkUserId]; - } else { - [RCAttributionPoster storePostponedAttributionData:data - fromNetwork:network - forNetworkUserId:networkUserId]; - } -} - -- (void)postAppleSearchAddsAttributionCollectionIfNeeded { - if (_automaticAppleSearchAdsAttributionCollection) { - [self.attributionPoster postAppleSearchAdsAttributionIfNeeded]; - } -} - -#pragma mark Identity - -- (NSString *)appUserID { - return [self.identityManager currentAppUserID]; -} - -- (BOOL)isAnonymous { - return [self.identityManager currentUserIsAnonymous]; -} - -- (void)createAlias:(NSString *)alias completionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - if ([alias isEqualToString:self.identityManager.currentAppUserID]) { - [self purchaserInfoWithCompletionBlock:completion]; - } else { - [self.identityManager createAliasForAppUserID:alias completion:^(NSError *_Nullable error) { - if (error == nil) { - [self updateAllCachesWithCompletionBlock:completion]; - } else { - if (completion) { - [self.operationDispatcher dispatchOnMainThread:^{ completion(nil, error); }]; - } - } - }]; - } -} - -- (void)identify:(NSString *)appUserID completionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - if ([appUserID isEqualToString:self.identityManager.currentAppUserID]) { - [self purchaserInfoWithCompletionBlock:completion]; - } else { - [self.identityManager identifyAppUserID:appUserID completion:^(NSError *error) { - if (error == nil) { - [self updateAllCachesWithCompletionBlock:completion]; - } else { - if (completion) { - [self.operationDispatcher dispatchOnMainThread:^{ completion(nil, error); }]; - } - } - }]; - - } -} - -- (void) logIn:(NSString *)appUserID -completionBlock:(void (^)(RCPurchaserInfo * _Nullable purchaserInfo, BOOL created, NSError * _Nullable error))completion { - [self.identityManager logInAppUserID:appUserID completion:^(RCPurchaserInfo *purchaserInfo, - BOOL created, - NSError * _Nullable error) { - [self.operationDispatcher dispatchOnMainThread:^{ completion(purchaserInfo, created, error); }]; - - if (error == nil) { - [self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { - [self.offeringsManager updateOfferingsCacheWithAppUserID:self.appUserID - isAppBackgrounded:isAppBackgrounded - completion:nil]; - }]; - } - }]; -} - -- (void)logOutWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - [self.identityManager logOutWithCompletion:^(NSError *error) { - if (error) { - if (completion) { - [self.operationDispatcher dispatchOnMainThread:^{ completion(nil, error); }]; - } - } else { - [self updateAllCachesWithCompletionBlock:completion]; - } - }]; -} - -- (void)resetWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - [self.identityManager resetAppUserID]; - [self updateAllCachesWithCompletionBlock:completion]; -} - -- (void)purchaserInfoWithCompletionBlock:(RCReceivePurchaserInfoBlock)completion { - [self.purchaserInfoManager purchaserInfoWithAppUserID:self.appUserID - completionBlock:completion]; -} - -#pragma mark Purchasing - -- (void)productsWithIdentifiers:(NSArray *)productIdentifiers - completionBlock:(RCReceiveProductsBlock)completion { - [self.purchasesOrchestrator productsWithIdentifiers:productIdentifiers completion:completion]; -} - -- (void)purchaseProduct:(SKProduct *)product - withCompletionBlock:(RCPurchaseCompletedBlock)completion { - SKMutablePayment *payment = [self.storeKitWrapper paymentWithProduct:product]; - [self purchaseProduct:product withPayment:payment withPresentedOfferingIdentifier:nil completion:completion]; -} - -- (void)purchasePackage:(RCPackage *)package - withCompletionBlock:(RCPurchaseCompletedBlock)completion { - SKMutablePayment *payment = [self.storeKitWrapper paymentWithProduct:package.product]; - [self purchaseProduct:package.product withPayment:payment withPresentedOfferingIdentifier:package.offeringIdentifier completion:completion]; -} - -- (void)purchaseProduct:(SKProduct *)product - withDiscount:(SKPaymentDiscount *)discount - completionBlock:(RCPurchaseCompletedBlock)completion { - SKMutablePayment *payment = [self.storeKitWrapper paymentWithProduct:product discount:discount]; - [self purchaseProduct:product withPayment:payment withPresentedOfferingIdentifier:nil completion:completion]; -} - -- (void)purchasePackage:(RCPackage *)package - withDiscount:(SKPaymentDiscount *)discount - completionBlock:(RCPurchaseCompletedBlock)completion { - SKMutablePayment *payment = [self.storeKitWrapper paymentWithProduct:package.product - discount:discount]; - [self purchaseProduct:package.product withPayment:payment withPresentedOfferingIdentifier:package.offeringIdentifier completion:completion]; -} - -- (void) purchaseProduct:(SKProduct *)product - withPayment:(SKMutablePayment *)payment -withPresentedOfferingIdentifier:(nullable NSString *)presentedOfferingIdentifier - completion:(RCPurchaseCompletedBlock)completion { - [self.purchasesOrchestrator purchaseProduct:product - payment:payment - presentedOfferingIdentifier:presentedOfferingIdentifier - completion:completion]; -} - -- (void)syncPurchasesWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - [self.purchasesOrchestrator syncPurchasesWithCompletion:completion]; -} - -- (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - [self.purchasesOrchestrator restoreTransactionsWithCompletion:completion]; -} - -// TODO: simplify logic, move to separate class -- (void)checkTrialOrIntroductoryPriceEligibility:(NSArray *)productIdentifiers - completionBlock:(RCReceiveIntroEligibilityBlock)receiveEligibility -{ - [self.receiptFetcher receiptDataWithRefreshPolicy:RCReceiptRefreshPolicyOnlyIfEmpty - completion:^(NSData * _Nullable data) { - if (data != nil && data.length > 0) { - if (@available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *)) { - NSSet *productIdentifiersSet = [[NSSet alloc] initWithArray:productIdentifiers]; - [self.introEligibilityCalculator checkTrialOrIntroductoryPriceEligibilityWith:data - productIdentifiers:productIdentifiersSet - completion:^(NSDictionary * _Nonnull receivedEligibility, - NSError * _Nullable error) { - if (!error) { - NSMutableDictionary *convertedEligibility = [[NSMutableDictionary alloc] init]; - - // TODO: remove enum conversion once this is moved to swift - for (NSString *key in receivedEligibility.allKeys) { - NSError *error = nil; - RCIntroEligibility *eligibility = [[RCIntroEligibility alloc] initWithEligibilityStatusCode:receivedEligibility[key] error:&error]; - if (!eligibility) { - [RCLog error:[NSString stringWithFormat:@"Unable to create an RCIntroEligibility: %@", - error.localizedDescription]]; - } else { - convertedEligibility[key] = eligibility; - } - } - [self.operationDispatcher dispatchOnMainThread:^{ receiveEligibility(convertedEligibility); }]; - } else { - // todo: unify all of these `else`s - [RCLog error:[NSString stringWithFormat:RCStrings.receipt.parse_receipt_locally_error, - error.localizedDescription]]; - [self.backend getIntroEligibilityForAppUserID:self.appUserID - receiptData:data - productIdentifiers:productIdentifiers - completion:^(NSDictionary * _Nonnull result, NSError * _Nullable error) { - [RCLog error:[NSString stringWithFormat:@"Unable to getIntroEligibilityForAppUserID: %@", - error.localizedDescription]]; - [self.operationDispatcher dispatchOnMainThread:^{ receiveEligibility(result); }]; - }]; - } - }]; - } else { - [self.backend getIntroEligibilityForAppUserID:self.appUserID - receiptData:data - productIdentifiers:productIdentifiers - completion:^(NSDictionary *_Nonnull result, NSError * _Nullable error) { - [RCLog error:[NSString stringWithFormat:@"Unable to getIntroEligibilityForAppUserID: %@", - error.localizedDescription]]; - [self.operationDispatcher dispatchOnMainThread:^{ receiveEligibility(result); }]; - }]; - } - } else { - [self.backend getIntroEligibilityForAppUserID:self.appUserID - receiptData:data - productIdentifiers:productIdentifiers - completion:^(NSDictionary * _Nonnull result, NSError * _Nullable error) { - [RCLog error:[NSString stringWithFormat:@"Unable to getIntroEligibilityForAppUserID: %@", - error.localizedDescription]]; - [self.operationDispatcher dispatchOnMainThread:^{ receiveEligibility(result); }]; - }]; - } - }]; -} - -- (void)paymentDiscountForProductDiscount:(SKProductDiscount *)discount - product:(SKProduct *)product - completion:(RCPaymentDiscountBlock)completion -API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)) { - [self.purchasesOrchestrator paymentDiscountForProductDiscount:discount product:product completion:completion]; -} - -- (void)invalidatePurchaserInfoCache { - [self.purchaserInfoManager clearPurchaserInfoCacheForAppUserID:self.appUserID]; -} - -- (void)presentCodeRedemptionSheet API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(tvos, macos, watchos) { - [self.storeKitWrapper presentCodeRedemptionSheet]; -} - -#pragma mark Subcriber Attributes - -- (void)setAttributes:(NSDictionary *)attributes { - [self.subscriberAttributesManager setAttributes:attributes appUserID:self.appUserID]; -} - -- (void)setEmail:(nullable NSString *)email { - [self.subscriberAttributesManager setEmail:email appUserID:self.appUserID]; -} - -- (void)setPhoneNumber:(nullable NSString *)phoneNumber { - [self.subscriberAttributesManager setPhoneNumber:phoneNumber appUserID:self.appUserID]; -} - -- (void)setDisplayName:(nullable NSString *)displayName { - [self.subscriberAttributesManager setDisplayName:displayName appUserID:self.appUserID]; -} - -- (void)setPushToken:(nullable NSData *)pushToken { - [self.subscriberAttributesManager setPushToken:pushToken appUserID:self.appUserID]; -} - -- (void)_setPushTokenString:(nullable NSString *)pushToken { - [self.subscriberAttributesManager setPushTokenString:pushToken appUserID:self.appUserID]; -} - -- (void)setAdjustID:(nullable NSString *)adjustID { - [self.subscriberAttributesManager setAdjustID:adjustID appUserID:self.appUserID]; -} - -- (void)setAppsflyerID:(nullable NSString *)appsflyerID { - [self.subscriberAttributesManager setAppsflyerID:appsflyerID appUserID:self.appUserID]; -} - -- (void)setFBAnonymousID:(nullable NSString *)fbAnonymousID { - [self.subscriberAttributesManager setFBAnonymousID:fbAnonymousID appUserID:self.appUserID]; -} - -- (void)setMparticleID:(nullable NSString *)mparticleID { - [self.subscriberAttributesManager setMparticleID:mparticleID appUserID:self.appUserID]; -} - -- (void)setOnesignalID:(nullable NSString *)onesignalID { - [self.subscriberAttributesManager setOnesignalID:onesignalID appUserID:self.appUserID]; -} - -- (void)setMediaSource:(nullable NSString *)mediaSource { - [self.subscriberAttributesManager setMediaSource:mediaSource appUserID:self.appUserID]; -} - -- (void)setCampaign:(nullable NSString *)campaign { - [self.subscriberAttributesManager setCampaign:campaign appUserID:self.appUserID]; -} - -- (void)setAdGroup:(nullable NSString *)adGroup { - [self.subscriberAttributesManager setAdGroup:adGroup appUserID:self.appUserID]; -} - -- (void)setAd:(nullable NSString *)ad { - [self.subscriberAttributesManager setAd:ad appUserID:self.appUserID]; -} - -- (void)setKeyword:(nullable NSString *)keyword { - [self.subscriberAttributesManager setKeyword:keyword appUserID:self.appUserID]; -} - -- (void)setCreative:(nullable NSString *)creative { - [self.subscriberAttributesManager setCreative:creative appUserID:self.appUserID]; -} - -- (void)collectDeviceIdentifiers { - [self.subscriberAttributesManager collectDeviceIdentifiersForAppUserID:self.appUserID]; -} - -#pragma mark - Private Methods - -- (void)applicationDidBecomeActive:(__unused NSNotification *)notif { - [RCLog debug:[NSString stringWithFormat:@"%@", RCStrings.configure.application_active]]; - [self updateAllCachesIfNeeded]; - [self syncSubscriberAttributesIfNeeded]; - [self postAppleSearchAddsAttributionCollectionIfNeeded]; -} - -- (void)applicationWillResignActive:(__unused NSNotification *)notif { - [self syncSubscriberAttributesIfNeeded]; -} - -- (void)updateAllCachesIfNeeded { - [self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { - [self.purchaserInfoManager fetchAndCachePurchaserInfoIfStaleWithAppUserID:self.appUserID - isAppBackgrounded:isAppBackgrounded - completion:nil]; - if ([self.deviceCache isOfferingsCacheStaleWithIsAppBackgrounded:isAppBackgrounded]) { - [RCLog debug:[NSString stringWithFormat:@"Offerings cache is stale, updating caches"]]; - [self.offeringsManager updateOfferingsCacheWithAppUserID:self.appUserID - isAppBackgrounded:isAppBackgrounded - completion:nil]; - } - }]; -} - -- (void)updateAllCachesWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { - [self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { - [self.purchaserInfoManager fetchAndCachePurchaserInfoWithAppUserID:self.appUserID - isAppBackgrounded:isAppBackgrounded - completion:completion]; - [self.offeringsManager updateOfferingsCacheWithAppUserID:self.appUserID - isAppBackgrounded:isAppBackgrounded - completion:nil]; - }]; -} - -- (void)offeringsWithCompletionBlock:(RCReceiveOfferingsBlock)completion { - [self.offeringsManager offeringsWithAppUserID:self.appUserID - completionBlock:completion]; -} - -- (void)syncSubscriberAttributesIfNeeded { - [self.operationDispatcher dispatchOnWorkerThreadWithRandomDelay:NO block:^{ - [self.subscriberAttributesManager syncAttributesForAllUsersWithCurrentAppUserID:self.appUserID]; - }]; -} - -// TODO make private after swift migration -- (void)markAttributesAsSyncedIfNeeded:(nullable RCSubscriberAttributeDict)syncedAttributes - appUserID:(NSString *)appUserID - error:(nullable NSError *)error { - if (error && !error.rc_successfullySynced) { - return; - } - - if (error.rc_subscriberAttributesErrors) { - [RCLog error:[NSString stringWithFormat:RCStrings.attribution.subscriber_attributes_error, - error.rc_subscriberAttributesErrors]]; - } - [self.subscriberAttributesManager markAttributesAsSynced:syncedAttributes appUserID:appUserID]; -} - -#pragma MARK: RCPurchaserInfoManagerDelegate -- (void)purchaserInfoManagerDidReceiveUpdatedPurchaserInfo:(RCPurchaserInfo *)purchaserInfo { - if (self.delegate && [self.delegate respondsToSelector:@selector(purchases:didReceiveUpdatedPurchaserInfo:)]) { - [self.delegate purchases:self didReceiveUpdatedPurchaserInfo:purchaserInfo]; - } -} - -#pragma MARK: RCPurchasesOrchestratorDelegate -- (void)shouldPurchasePromoProduct:(SKProduct * _Nonnull)product - defermentBlock:(void (^ _Nonnull)(void (^ _Nonnull)(SKPaymentTransaction * _Nullable, RCPurchaserInfo * _Nullable, NSError * _Nullable, BOOL)))defermentBlock { - if (self.delegate && [self.delegate respondsToSelector:@selector(purchases:shouldPurchasePromoProduct:defermentBlock:)]) { - [self.delegate purchases:self shouldPurchasePromoProduct:product defermentBlock:defermentBlock]; - } -} - -@end diff --git a/PurchasesCoreSwift/Identity/PurchaserInfoManager.swift b/PurchasesCoreSwift/Identity/PurchaserInfoManager.swift index 0e20bbe9e7..9c8758a105 100644 --- a/PurchasesCoreSwift/Identity/PurchaserInfoManager.swift +++ b/PurchasesCoreSwift/Identity/PurchaserInfoManager.swift @@ -13,8 +13,6 @@ import Foundation -public typealias ReceivePurchaserInfoBlock = (PurchaserInfo?, Error?) -> Void - @objc(RCPurchaserInfoManagerDelegate) public protocol PurchaserInfoManagerDelegate: NSObjectProtocol { @objc(purchaserInfoManagerDidReceiveUpdatedPurchaserInfo:) diff --git a/PurchasesCoreSwift/Logging/Strings/PurchaseStrings.swift b/PurchasesCoreSwift/Logging/Strings/PurchaseStrings.swift index c04b81789b..a8b9ce1f1b 100644 --- a/PurchasesCoreSwift/Logging/Strings/PurchaseStrings.swift +++ b/PurchasesCoreSwift/Logging/Strings/PurchaseStrings.swift @@ -43,4 +43,12 @@ class PurchaseStrings { "Apple returned a payment where the productIdentifier is nil, this is possibly an App Store quirk" } + var purchases_nil: String { "Purchases has not been configured. Please call Purchases.configure()" } + + var purchases_delegate_set_multiple_times: String { "Purchases delegate has already been configured." } + + var purchases_delegate_set_to_nil: String { + "Purchases delegate is being set to nil, you probably don't want to do this." + } + } diff --git a/PurchasesCoreSwift/Misc/PurchasesTestStandIn.swift b/PurchasesCoreSwift/Misc/PurchasesTestStandIn.swift new file mode 100644 index 0000000000..746f2308fb --- /dev/null +++ b/PurchasesCoreSwift/Misc/PurchasesTestStandIn.swift @@ -0,0 +1,22 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PurchasesTestStandIn.swift +// +// Created by Joshua Liebowitz on 8/20/21. + +import Foundation + +// This class is used during unit testing on ARM64. We cannot check for fatalError() in tests. Instead of skipping +// testing on that platform for the fatalError() that should occur when we call .sharedPurchases before +// .configure/init, we substitute this class as the return and check for it. If this class is used at all, which +// it shouldn't be due to internal visibility. +class PurchasesTestStandIn: Purchases { + +} diff --git a/PurchasesCoreSwift/Public/Purchases.swift b/PurchasesCoreSwift/Public/Purchases.swift new file mode 100644 index 0000000000..ec314645c7 --- /dev/null +++ b/PurchasesCoreSwift/Public/Purchases.swift @@ -0,0 +1,1284 @@ +// +// 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 +// +// Purchases.swift +// +// Created by Joshua Liebowitz on 8/18/21. + +// swiftlint:disable file_length function_parameter_count type_body_length +import Foundation +import StoreKit + +// MARK: Block definitions +// NOTE: Any changes here must be reflected in PurchasesCoreSwift.h for ObjC compatibility. +/** + Completion block for calls that send back a `PurchaserInfo` + */ +public typealias ReceivePurchaserInfoBlock = (PurchaserInfo?, Error?) -> Void + +/** + Completion block for `-[Purchases checkTrialOrIntroductoryPriceEligibility:completionBlock:]` + */ +public typealias ReceiveIntroEligibilityBlock = ([String: IntroEligibility]) -> Void + +/** + Completion block for `-[Purchases offeringsWithCompletionBlock:]` + */ + +public typealias ReceiveOfferingsBlock = (Offerings?, Error?) -> Void + +/** + Completion block for `-[Purchases productsWithIdentifiers:completionBlock:]` + */ +public typealias ReceiveProductsBlock = ([SKProduct]) -> Void + +/** + Completion block for `-[Purchases purchaseProduct:withCompletionBlock:]` + */ +public typealias PurchaseCompletedBlock = (SKPaymentTransaction?, PurchaserInfo?, Error?, Bool) -> Void +public typealias RCPurchaseCompletedBlock = PurchaseCompletedBlock + +/** + Deferred block for `purchases:shouldPurchasePromoProduct:defermentBlock:` + */ +public typealias DeferredPromotionalPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void + +/** + * Deferred block for `-[Purchases paymentDiscountForProductDiscount:product:completion:]` + */ +@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) +public typealias PaymentDiscountBlock = (SKPaymentDiscount?, Error?) -> Void + +/** + * `Purchases` is the entry point for Purchases.framework. It should be instantiated as soon as your app has a unique + * user id for your user. This can be when a user logs in if you have accounts or on launch if you can generate a random + * user identifier. + * - Warning: Only one instance of Purchases should be instantiated at a time! Use a configure method to let the + * framework handle the singleton instance for you. + */ +@objc(RCPurchases) public class Purchases: NSObject { + + @objc(sharedPurchases) + public static var shared: Purchases { + if let purchases = purchases { + return purchases + } + + notConfiguredAssertionFunction() + + // This will only be returned during testing. #hack for ARM64 testing fatalError() + // See https://github.com/Quick/Nimble/blob/main/Sources/Nimble/Matchers/ThrowAssertion.swift#L87 + return PurchasesTestStandIn(apiKey: "StandIn", appUserID: nil) + } + private static var purchases: Purchases? + + @objc public static var isConfigured: Bool { purchases != nil } + + /** + * Delegate for `Purchases` instance. The delegate is responsible for handling promotional product purchases and + * changes to purchaser information. + */ + @objc public var delegate: PurchasesDelegate? { + get { privateDelegate } + set { + guard newValue !== privateDelegate else { + Logger.warn(Strings.purchase.purchases_delegate_set_multiple_times) + return + } + + if newValue == nil { + Logger.info(Strings.purchase.purchases_delegate_set_to_nil) + } + + privateDelegate = newValue + purchaserInfoManager.delegate = self + purchaserInfoManager.sendCachedPurchaserInfoIfAvailable(appUserID: appUserID) + Logger.debug(Strings.configure.delegate_set) + } + } + + private weak var privateDelegate: PurchasesDelegate? + private let operationDispatcher: OperationDispatcher + + /** + * Enable automatic collection of Apple Search Ads attribution. Disabled by default + */ + @objc public static var automaticAppleSearchAdsAttributionCollection: Bool = false + + /** + * Used to set the log level. Useful for debugging issues with the lovely team @RevenueCat + */ + @objc public static var logLevel: LogLevel { + get { Logger.logLevel } + set { Logger.logLevel = newValue } + } + + /** + * Enable debug logging. Useful for debugging issues with the lovely team @RevenueCat. + */ + @available(*, deprecated, message: "use Purchases.logLevel instead.") + @objc public static var debugLogsEnabled: Bool { + get { logLevel == .debug } + set { logLevel = newValue ? .debug : .info } + } + + /** + * Set this property to your proxy URL before configuring Purchases *only* if you've received a proxy key value + * from your RevenueCat contact. + */ + @objc public static var proxyURL: URL? { + get { SystemInfo.proxyURL } + set { SystemInfo.proxyURL = newValue } + } + + /** + * Set this property to true *only* if you're transitioning an existing Mac app from the Legacy + * Mac App Store into the Universal Store, and you've configured your RevenueCat app accordingly. + * Contact support before using this. + */ + @objc public static var forceUniversalAppStore: Bool { + get { SystemInfo.forceUniversalAppStore } + set { SystemInfo.forceUniversalAppStore = newValue } + } + + /** + * Set this property to true *only* when testing the ask-to-buy / SCA purchases flow. More information: + * http://errors.rev.cat/ask-to-buy + */ + @available(iOS 8.0, macOS 10.14, watchOS 6.2, macCatalyst 13.0, *) + @objc public static var simulatesAskToBuyInSandbox: Bool { + get { StoreKitWrapper.simulatesAskToBuyInSandbox } + set { StoreKitWrapper.simulatesAskToBuyInSandbox = newValue } + } + + /** + * Indicates whether the user is allowed to make payments. + */ + @objc public static func canMakePayments() -> Bool { SKPaymentQueue.canMakePayments() } + + /** + * Set a custom log handler for redirecting logs to your own logging system. + * By default, this sends Info, Warn, and Error messages. If you wish to receive Debug level messages, + * you must enable debug logs. + */ + @objc public static var logHandler: (LogLevel, String) -> Void { + get { Logger.logHandler } + set { Logger.logHandler = newValue } + } + + /// Current version of the Purchases framework. + @objc public static var frameworkVersion: String { SystemInfo.frameworkVersion } + + @objc public var allowSharingAppStoreAccount: Bool { + get { purchasesOrchestrator.allowSharingAppStoreAccount } + set { purchasesOrchestrator.allowSharingAppStoreAccount = newValue } + } + + @objc public var finishTransactions: Bool { + get { systemInfo.finishTransactions } + set { systemInfo.finishTransactions = newValue } + } + + private let attributionFetcher: AttributionFetcher + private let attributionPoster: AttributionPoster + private let backend: Backend + private let deviceCache: DeviceCache + private let identityManager: IdentityManager + private let introEligibilityCalculator: IntroEligibilityCalculator + private let notificationCenter: NotificationCenter + private let offeringsFactory: OfferingsFactory + private let offeringsManager: OfferingsManager + private let productsManager: ProductsManager + private let purchaserInfoManager: PurchaserInfoManager + private let purchasesOrchestrator: PurchasesOrchestrator + private let receiptFetcher: ReceiptFetcher + private let receiptParser: ReceiptParser + private let requestFetcher: StoreKitRequestFetcher + private let storeKitWrapper: StoreKitWrapper + private let subscriberAttributesManager: SubscriberAttributesManager + private let systemInfo: SystemInfo + + fileprivate static let initLock = NSLock() + + static var notConfiguredAssertionFunction: () -> Void = { fatalError(Strings.purchase.purchases_nil) } + + @objc convenience init(apiKey: String, appUserID: String?) { + self.init(apiKey: apiKey, + appUserID: appUserID, + userDefaults: nil, + observerMode: false, + platformFlavor: nil, + platformFlavorVersion: nil) + } + + @objc public convenience init(apiKey: String, + appUserID: String?, + userDefaults: UserDefaults?, + observerMode: Bool, + platformFlavor: String?, + platformFlavorVersion: String?) { + let operationDispatcher = OperationDispatcher() + let receiptRefreshRequestFactory = ReceiptRefreshRequestFactory() + let fetcher = StoreKitRequestFetcher(requestFactory: receiptRefreshRequestFactory, + operationDispatcher: operationDispatcher) + let receiptFetcher = ReceiptFetcher(requestFetcher: fetcher) + let systemInfo: SystemInfo + do { + systemInfo = try SystemInfo(platformFlavor: platformFlavor, + platformFlavorVersion: platformFlavorVersion, + finishTransactions: !observerMode) + } catch { + fatalError(error.localizedDescription) + } + + let eTagManager = ETagManager() + let backend = Backend(apiKey: apiKey, + systemInfo: systemInfo, + eTagManager: eTagManager, + operationDispatcher: operationDispatcher) + let storeKitWrapper = StoreKitWrapper() + let offeringsFactory = OfferingsFactory() + let userDefaults = userDefaults ?? UserDefaults.standard + let deviceCache = DeviceCache(userDefaults: userDefaults) + let introCalculator = IntroEligibilityCalculator() + let receiptParser = ReceiptParser() + let purchaserInfoManager = PurchaserInfoManager(operationDispatcher: operationDispatcher, + deviceCache: deviceCache, + backend: backend, + systemInfo: systemInfo) + let identityManager = IdentityManager(deviceCache: deviceCache, + backend: backend, + purchaserInfoManager: purchaserInfoManager) + let attributionTypeFactory = AttributionTypeFactory() + let attributionFetcher = AttributionFetcher(attributionFactory: attributionTypeFactory, systemInfo: systemInfo) + let attributionDataMigrator = AttributionDataMigrator() + let subscriberAttributesManager = SubscriberAttributesManager(backend: backend, + deviceCache: deviceCache, + attributionFetcher: attributionFetcher, + attributionDataMigrator: attributionDataMigrator) + let attributionPoster = AttributionPoster(deviceCache: deviceCache, + identityManager: identityManager, + backend: backend, + attributionFetcher: attributionFetcher, + subscriberAttributesManager: subscriberAttributesManager) + let productsRequestFactory = ProductsRequestFactory() + let productsManager = ProductsManager(productsRequestFactory: productsRequestFactory) + let offeringsManager = OfferingsManager(deviceCache: deviceCache, + operationDispatcher: operationDispatcher, + systemInfo: systemInfo, + backend: backend, + offeringsFactory: offeringsFactory, + productsManager: productsManager) + let purchasesOrchestrator = PurchasesOrchestrator(productsManager: productsManager, + storeKitWrapper: storeKitWrapper, + systemInfo: systemInfo, + subscriberAttributesManager: subscriberAttributesManager, + operationDispatcher: operationDispatcher, + receiptFetcher: receiptFetcher, + purchaserInfoManager: purchaserInfoManager, + backend: backend, + identityManager: identityManager, + receiptParser: receiptParser, + deviceCache: deviceCache) + self.init(appUserID: appUserID, + requestFetcher: fetcher, + receiptFetcher: receiptFetcher, + attributionFetcher: attributionFetcher, + attributionPoster: attributionPoster, + backend: backend, + storeKitWrapper: storeKitWrapper, + notificationCenter: NotificationCenter.default, + systemInfo: systemInfo, + offeringsFactory: offeringsFactory, + deviceCache: deviceCache, + identityManager: identityManager, + subscriberAttributesManager: subscriberAttributesManager, + operationDispatcher: operationDispatcher, + introEligibilityCalculator: introCalculator, + receiptParser: receiptParser, + purchaserInfoManager: purchaserInfoManager, + productsManager: productsManager, + offeringsManager: offeringsManager, + purchasesOrchestrator: purchasesOrchestrator) + } + + init(appUserID: String?, + requestFetcher: StoreKitRequestFetcher, + receiptFetcher: ReceiptFetcher, + attributionFetcher: AttributionFetcher, + attributionPoster: AttributionPoster, + backend: Backend, + storeKitWrapper: StoreKitWrapper, + notificationCenter: NotificationCenter, + systemInfo: SystemInfo, + offeringsFactory: OfferingsFactory, + deviceCache: DeviceCache, + identityManager: IdentityManager, + subscriberAttributesManager: SubscriberAttributesManager, + operationDispatcher: OperationDispatcher, + introEligibilityCalculator: IntroEligibilityCalculator, + receiptParser: ReceiptParser, + purchaserInfoManager: PurchaserInfoManager, + productsManager: ProductsManager, + offeringsManager: OfferingsManager, + purchasesOrchestrator: PurchasesOrchestrator) { + + Logger.debug(Strings.configure.debug_enabled) + Logger.debug(String(format: Strings.configure.sdk_version, Self.frameworkVersion)) + Logger.user(String(format: Strings.configure.initial_app_user_id, appUserID ?? "nil appUserID")) + + self.requestFetcher = requestFetcher + self.receiptFetcher = receiptFetcher + self.attributionFetcher = attributionFetcher + self.attributionPoster = attributionPoster + self.backend = backend + self.storeKitWrapper = storeKitWrapper + self.offeringsFactory = offeringsFactory + self.deviceCache = deviceCache + self.identityManager = identityManager + self.notificationCenter = notificationCenter + self.systemInfo = systemInfo + self.subscriberAttributesManager = subscriberAttributesManager + self.operationDispatcher = operationDispatcher + self.introEligibilityCalculator = introEligibilityCalculator + self.receiptParser = receiptParser + self.purchaserInfoManager = purchaserInfoManager + self.productsManager = productsManager + self.offeringsManager = offeringsManager + self.purchasesOrchestrator = purchasesOrchestrator + + super.init() + + identityManager.configure(appUserID: appUserID) + self.purchasesOrchestrator.maybeDelegate = self + + systemInfo.isApplicationBackgrounded { isBackgrounded in + if isBackgrounded { + self.purchaserInfoManager.sendCachedPurchaserInfoIfAvailable(appUserID: self.appUserID) + } else { + self.operationDispatcher.dispatchOnWorkerThread { + self.updateAllCaches(completion: nil) + } + } + } + + storeKitWrapper.delegate = purchasesOrchestrator + subscribeToAppStateNotifications() + attributionPoster.postPostponedAttributionDataIfNeeded() + postAppleSearchAddsAttributionCollectionIfNeeded() + + } + + /** + * Automatically collect subscriber attributes associated with the device identifiers + * $idfa, $idfv, $ip + */ + @objc func collectDeviceIdentifiers() { + subscriberAttributesManager.collectDeviceIdentifiers(forAppUserID: appUserID) + } + + deinit { + notificationCenter.removeObserver(self) + storeKitWrapper.delegate = nil + purchaserInfoManager.delegate = nil + privateDelegate = nil + Self.automaticAppleSearchAdsAttributionCollection = false + Self.proxyURL = nil + } + + static func clearSingleton() { + Self.purchases = nil + } + + static func setDefaultInstance(_ purchases: Purchases) { + initLock.lock() + if isConfigured { + Logger.info(Strings.configure.purchase_instance_already_set) + } + + Self.purchases = purchases + initLock.unlock() + } + +} + +// MARK: SubscriberAttributesManager Setters. +extension Purchases { + + /** + * Subscriber attributes are useful for storing additional, structured information on a user. + * Since attributes are writable using a public key they should not be used for + * managing secure or sensitive information such as subscription status, coins, etc. + * + * Key names starting with "$" are reserved names used by RevenueCat. For a full list of key + * restrictions refer to our guide: https://docs.revenuecat.com/docs/subscriber-attributes + * + * - Parameter attributes: Map of attributes by key. Set the value as an empty string to delete an attribute. + */ + @objc public func setAttributes(_ attributes: [String: String]) { + subscriberAttributesManager.setAttributes(attributes, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the email address for the user + * + * - Parameter email: Empty String or nil will delete the subscriber attribute. + */ + @objc public func setEmail(_ email: String) { + subscriberAttributesManager.setEmail(email, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the phone number for the user + * + * - Parameter phoneNumber: Empty String or nil will delete the subscriber attribute. + */ + @objc public func setPhoneNumber(_ phoneNumber: String) { + subscriberAttributesManager.setPhoneNumber(phoneNumber, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the display name for the user + * + * - Parameter displayName: Empty String or nil will delete the subscriber attribute. + */ + @objc public func setDisplayName(_ displayName: String) { + subscriberAttributesManager.setDisplayName(displayName, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the push token for the user + * + * - Parameter pushToken: nil will delete the subscriber attribute. + */ + @objc public func setPushToken(_ pushToken: Data) { + subscriberAttributesManager.setPushToken(pushToken, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the Adjust Id for the user + * Required for the RevenueCat Adjust integration + * + * - Parameter adjustID: nil will delete the subscriber attribute + */ + @objc public func setAdjustID(_ adjustID: String) { + subscriberAttributesManager.setAdjustID(adjustID, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the Appsflyer Id for the user + * Required for the RevenueCat Appsflyer integration + * + * - Parameter appsflyerID: nil will delete the subscriber attribute + */ + @objc public func setAppsflyerID(_ appsflyerID: String) { + subscriberAttributesManager.setAppsflyerID(appsflyerID, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the Facebook SDK Anonymous Id for the user + * Recommended for the RevenueCat Facebook integration + * + * - Parameter fbAnonymousID: nil will delete the subscriber attribute + */ + @objc public func setFBAnonymousID(_ fbAnonymousID: String) { + subscriberAttributesManager.setFBAnonymousID(fbAnonymousID, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the mParticle Id for the user + * Recommended for the RevenueCat mParticle integration + * + * - Parameter mparticleID: nil will delete the subscriber attribute + */ + @objc public func setMparticleID(_ mparticleID: String) { + subscriberAttributesManager.setMparticleID(mparticleID, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the OneSignal Player Id for the user + * Required for the RevenueCat OneSignal integration + * + * - Parameter onesignalID: nil will delete the subscriber attribute + */ + @objc public func setOnesignalID(_ onesignalID: String) { + subscriberAttributesManager.setOnesignalID(onesignalID, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install media source for the user + * + * - Parameter mediaSource: nil will delete the subscriber attribute. + */ + @objc public func setMediaSource(_ mediaSource: String) { + subscriberAttributesManager.setMediaSource(mediaSource, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install campaign for the user + * + * - Parameter campaign: nil will delete the subscriber attribute. + */ + @objc public func setCampaign(_ campaign: String) { + subscriberAttributesManager.setCampaign(campaign, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install ad group for the user + * + * - Parameter adGroup: nil will delete the subscriber attribute. + */ + @objc public func setAdGroup(_ adGroup: String) { + subscriberAttributesManager.setAdGroup(adGroup, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install ad for the user + * + * - Parameter ad: nil will delete the subscriber attribute. + */ + @objc public func setAd(_ ad: String) { + subscriberAttributesManager.setAd(ad, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install keyword for the user + * + * - Parameter keyword: nil will delete the subscriber attribute. + */ + @objc public func setKeyword(_ keyword: String) { + subscriberAttributesManager.setKeyword(keyword, appUserID: appUserID) + } + + /** + * Subscriber attribute associated with the install ad creative for the user. + * + * - Parameter creative: nil will delete the subscriber attribute. + */ + @objc public func setCreative(_ creative: String) { + subscriberAttributesManager.setCreative(creative, appUserID: appUserID) + } + + func setPushTokenString(_ pushToken: String) { + subscriberAttributesManager.setPushTokenString(pushToken, appUserID: appUserID) + } + +} + +// MARK: Attribution. +extension Purchases { + + /** + * Send your attribution data to RevenueCat so you can track the revenue generated by your different campaigns. + * + * - Parameter data: Dictionary provided by the network. See https://docs.revenuecat.com/docs/attribution + * - Parameter network: Enum for the network the data is coming from, see `RCAttributionNetwork` for supported + * networks. + */ + @objc public static func addAttributionData(_ data: [String: Any], fromNetwork network: AttributionNetwork) { + addAttributionData(data, fromNetwork: network, forNetworkUserId: nil) + } + + /** + * Send your attribution data to RevenueCat so you can track the revenue generated by your different campaigns. + * + * - Parameter data: Dictionary provided by the network. See https://docs.revenuecat.com/docs/attribution + * - Parameter network: Enum for the network the data is coming from, see `RCAttributionNetwork` for supported + * networks. + * - Parameter maybeNetworkUserId: User Id that should be sent to the network. Default is the current App User Id. + */ + @objc public static func addAttributionData(_ data: [String: Any], + fromNetwork network: AttributionNetwork, + forNetworkUserId maybeNetworkUserId: String?) { + if Self.isConfigured { + shared.post(attributionData: data, fromNetwork: network, forNetworkUserId: maybeNetworkUserId) + } else { + AttributionPoster.store(postponedAttributionData: data, + fromNetwork: network, + forNetworkUserId: maybeNetworkUserId) + } + } + + /** + * Send your attribution data to RevenueCat so you can track the revenue generated by your different campaigns. + * + * - Parameter data: Dictionary provided by the network. See https://docs.revenuecat.com/docs/attribution + * - Parameter network: Enum for the network the data is coming from, see `RCAttributionNetwork` for supported + * networks. + * - Parameter networkUserId: User Id that should be sent to the network. Default is the current App User Id. + */ + public static func addAttributionData(_ data: [String: Any], + from network: AttributionNetwork, + forNetworkUserId networkUserId: String?) { + addAttributionData(data, fromNetwork: network, forNetworkUserId: networkUserId) + } + + private func post(attributionData data: [String: Any], + fromNetwork network: AttributionNetwork, + forNetworkUserId networkUserId: String?) { + attributionPoster.post(attributionData: data, fromNetwork: network, forNetworkUserId: networkUserId) + } + + private func postAppleSearchAddsAttributionCollectionIfNeeded() { + guard Self.automaticAppleSearchAdsAttributionCollection else { + return + } + attributionPoster.postAppleSearchAdsAttributionIfNeeded() + } + +} + +// MARK: Identity +public extension Purchases { + + /** + * The `appUserID` used by `Purchases`. + * If not passed on initialization this will be generated and cached by `Purchases`. + */ + @objc var appUserID: String { identityManager.currentAppUserID } + + /// If the `appUserID` has been generated by RevenueCat + @objc var isAnonymous: Bool { identityManager.currentUserIsAnonymous } + + /** + * This function will alias two appUserIDs together. + * + * - Parameter alias: The new appUserID that should be linked to the currently identified appUserID + * - Parameter maybeCompletionBlock: An optional completion block called when the aliasing has been successful. + * This completion block will receive an error if there's been one. + */ + @objc func createAlias(_ alias: String, completionBlock maybeCompletionBlock: ReceivePurchaserInfoBlock?) { + if alias == appUserID { + purchaserInfoManager.purchaserInfo(appUserID: appUserID, completion: maybeCompletionBlock) + } else { + identityManager.createAlias(appUserID: alias) { maybeError in + guard maybeError == nil else { + if let completion = maybeCompletionBlock { + self.operationDispatcher.dispatchOnMainThread { + completion(nil, maybeError) + } + } + return + } + + self.updateAllCaches(completion: maybeCompletionBlock) + } + } + } + + /** + * This function will identify the current user with an appUserID. Typically this would be used after a + * logout to identify a new user without calling configure. + * + * - Parameter appUserID: The appUserID that should be linked to the current user. + */ + @objc func identify(_ appUserID: String, completionBlock maybeCompletion: ReceivePurchaserInfoBlock?) { + if appUserID == identityManager.currentAppUserID { + purchaserInfoManager.purchaserInfo(appUserID: self.appUserID, completion: maybeCompletion) + } else { + identityManager.identify(appUserID: appUserID) { maybeError in + guard maybeError == nil else { + if let completion = maybeCompletion { + self.operationDispatcher.dispatchOnMainThread { + completion(nil, maybeError) + } + } + return + } + self.updateAllCaches(completion: maybeCompletion) + } + } + } + + /** + * This function will logIn the current user with an appUserID. + * + * - Parameter appUserID: The appUserID that should be linked to the current user. + * + * The callback will be called with the latest PurchaserInfo for the user, as well as a boolean + * indicating whether the user was created for the first time in the RevenueCat backend. + * See https://docs.revenuecat.com/docs/user-ids + */ + @objc func logIn(appUserID: String, completionBlock completion: @escaping (PurchaserInfo?, Bool, Error?) -> Void) { + identityManager.logIn(appUserID: appUserID) { purchaserInfo, created, maybeError in + self.operationDispatcher.dispatchOnMainThread { + completion(purchaserInfo, created, maybeError) + } + + guard maybeError == nil else { + return + } + + self.systemInfo.isApplicationBackgrounded { isAppBackgrounded in + self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, + isAppBackgrounded: + isAppBackgrounded, + completion: nil) + } + } + } + + /** + * Logs out the Purchases client clearing the saved appUserID. + * This will generate a random user id and save it in the cache. + * If this method is called and the current user is anonymous, it will return an error. + * See https://docs.revenuecat.com/docs/user-ids + */ + @objc func logOut(completionBlock maybeCompletion: ReceivePurchaserInfoBlock?) { + identityManager.logOut { maybeError in + guard maybeError == nil else { + if let completion = maybeCompletion { + self.operationDispatcher.dispatchOnMainThread { + completion(nil, maybeError) + } + } + return + } + + self.updateAllCaches(completion: maybeCompletion) + } + } + + /** + * Resets the Purchases client clearing the saved appUserID. + * This will generate a random user id and save it in the cache. + */ + @objc func reset(completionBlock maybeCompletion: ReceivePurchaserInfoBlock?) { + identityManager.resetAppUserID() + updateAllCaches(completion: maybeCompletion) + } + + /** + * Fetch the configured offerings for this users. Offerings allows you to configure your in-app products + * via RevenueCat and greatly simplifies management. + * See the guide (https://docs.revenuecat.com/entitlements) for more info. + * + * Offerings will be fetched and cached on instantiation so that, by the time they are needed, + * your prices are loaded for your purchase flow. Time is money. + * + * - Parameter completion: A completion block called when offerings are available. + * Called immediately if offerings are cached. Offerings will be nil if an error occurred. + */ + @objc func offerings(completionBlock completion: @escaping ReceiveOfferingsBlock) { + offeringsManager.offerings(appUserID: appUserID, completion: completion) + } + +} + +// MARK: Purchasing +public extension Purchases { + + /** + * Get latest available purchaser info. + * + * - Parameter completion: A completion block called when purchaser info is available and not stale. + * Called immediately if purchaser info is cached. Purchaser info can be nil * if an error occurred. + */ + @objc func purchaserInfo(completionBlock completion: @escaping ReceivePurchaserInfoBlock) { + purchaserInfoManager.purchaserInfo(appUserID: appUserID, completion: completion) + } + + /** + * Fetches the `SKProducts` for your IAPs for given `productIdentifiers`. + * Use this method if you aren't using `-offeringsWithCompletionBlock:`. + * You should use offerings though. + * + * - Note: `completion` may be called without `SKProduct`s that you are expecting. This is usually caused by + * iTunesConnect configuration errors. Ensure your IAPs have the "Ready to Submit" status in iTunesConnect. + * Also ensure that you have an active developer program subscription and you have signed the latest paid + * application agreements. + * If you're having trouble see: https://www.revenuecat.com/2018/10/11/configuring-in-app-products-is-hard + * + * - Parameter identifiers: A set of product identifiers for in app purchases setup via iTunesConnect. + * This should be either hard coded in your application, from a file, or from a custom endpoint if you want + * to be able to deploy new IAPs without an app update. + * - Parameter completion: An @escaping callback that is called with the loaded products. + * If the fetch fails for any reason it will return an empty array. + */ + @objc func products(identifiers: [String], completionBlock completion: @escaping ([SKProduct]) -> Void) { + purchasesOrchestrator.products(withIdentifiers: identifiers, completion: completion) + } + + /** + * Use this function if you are not using the Offerings system to purchase an `SKProduct`. + * If you are using the Offerings system, use `Purchases.purchase(package:completion:)` instead. + * + * Call this method when a user has decided to purchase a product. Only call this in direct response to user input. + * + * From here `Purchases` will handle the purchase with `StoreKit` and call the `PurchaseCompletedBlock`. + * + * - Note: You do not need to finish the transaction yourself in the completion callback, Purchases will + * handle this for you. + * + * - Parameter product: The `SKProduct` the user intends to purchase + * - Parameter completion: A completion block that is called when the purchase completes. + * + * If the purchase was successful there will be a `SKPaymentTransaction` and a `PurchaserInfo`. + * + * If the purchase was not successful, there will be an `NSError`. + * + * If the user cancelled, `userCancelled` will be `YES`. + */ + @objc(purchaseProduct:withCompletionBlock:) + func purchase(product: SKProduct, completion: @escaping PurchaseCompletedBlock) { + let payment: SKMutablePayment = storeKitWrapper.payment(withProduct: product) + purchase(product: product, payment: payment, presentedOfferingIdentifier: nil, completion: completion) + } + + /** + * Purchase the passed `Package`. + * Call this method when a user has decided to purchase a product. Only call this in direct response to user input. + * From here `Purchases` will handle the purchase with `StoreKit` and call the `PurchaseCompletedBlock`. + * + * - Note: You do not need to finish the transaction yourself in the completion callback, Purchases will + * handle this for you. + * + * - Parameter package: The `Package` the user intends to purchase + * - Parameter completion: A completion block that is called when the purchase completes. + * + * If the purchase was successful there will be a `SKPaymentTransaction` and a `PurchaserInfo`. + * + * If the purchase was not successful, there will be an `NSError`. + * + * If the user cancelled, `userCancelled` will be `YES`. + */ + @objc(purchasePackage:withCompletionBlock:) + func purchase(package: Package, completion: @escaping PurchaseCompletedBlock) { + let payment = storeKitWrapper.payment(withProduct: package.product) + purchase(product: package.product, + payment: payment, + presentedOfferingIdentifier: package.offeringIdentifier, + completion: completion) + } + + /** + * Use this function if you are not using the Offerings system to purchase an `SKProduct` with an + * applied `SKPaymentDiscount`. + * If you are using the Offerings system, use `Purchases.purchase(package:discount:completion:)` instead. + * + * Call this method when a user has decided to purchase a product with an applied discount. + * Only call this in direct response to user input. + * + * From here `Purchases` will handle the purchase with `StoreKit` and call the `PurchaseCompletedBlock`. + * + * - Note: You do not need to finish the transaction yourself in the completion callback, Purchases will handle + * this for you. + * + * - Parameter product: The `SKProduct` the user intends to purchase + * - Parameter discount: The `SKPaymentDiscount` to apply to the purchase + * - Parameter completion: A completion block that is called when the purchase completes. + * + * If the purchase was successful there will be a `SKPaymentTransaction` and a `PurchaserInfo`. + * If the purchase was not successful, there will be an `NSError`. + * If the user cancelled, `userCancelled` will be `YES`. + */ + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) + @objc(purchaseProduct:withDiscount:completionBlock:) + func purchase(product: SKProduct, discount: SKPaymentDiscount, completion: @escaping PurchaseCompletedBlock) { + let payment = storeKitWrapper.payment(withProduct: product, discount: discount) + purchase(product: product, payment: payment, presentedOfferingIdentifier: nil, completion: completion) + } + + /** + * Purchase the passed `Package`. + * Call this method when a user has decided to purchase a product with an applied discount. Only call this in + * direct response to user input. From here `Purchases` will handle the purchase with `StoreKit` and call the + * `PurchaseCompletedBlock`. + * + * - Note: You do not need to finish the transaction yourself in the completion callback, Purchases will handle + * this for you. + * + * - Parameter package: The `Package` the user intends to purchase + * - Parameter discount: The `PaymentDiscount` to apply to the purchase + * - Parameter completion: A completion block that is called when the purchase completes. + * + * If the purchase was successful there will be a `SKPaymentTransaction` and a * `PurchaserInfo`. + * If the purchase was not successful, there will be an `Error`. + * If the user cancelled, `userCancelled` will be `YES`. + */ + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) + @objc(purchasePackage:withDiscount:completionBlock:) + func purchase(package: Package, discount: SKPaymentDiscount, completion: @escaping PurchaseCompletedBlock) { + let payment = storeKitWrapper.payment(withProduct: package.product, discount: discount) + purchase(product: package.product, + payment: payment, + presentedOfferingIdentifier: package.offeringIdentifier, + completion: completion) + } + + /** + * This method will post all purchases associated with the current App Store account to RevenueCat and + * become associated with the current `appUserID`. + * + * If the receipt is being used by an existing user, the current `appUserID` will be aliased together with + * the `appUserID` of the existing user. + * Going forward, either `appUserID` will be able to reference the same user. + * + * - Warning: This function should only be called if you're not calling any purchase method. + * + * - Note: This method will not trigger a login prompt from App Store. However, if the receipt currently + * on the device does not contain subscriptions, but the user has made subscription purchases, this method + * won't be able to restore them. Use restoreTransactionsWithCompletionBlock to cover those cases. + */ + @objc func syncPurchases(completionBlock completion: ReceivePurchaserInfoBlock?) { + purchasesOrchestrator.syncPurchases(completion: completion) + } + + /** + * This method will post all purchases associated with the current App Store account to RevenueCat and become + * associated with the current `appUserID`. If the receipt is being used by an existing user, the current + * `appUserID` will be aliased together with the `appUserID` of the existing user. + * Going forward, either `appUserID` will be able to reference the same user. + * + * You shouldn't use this method if you have your own account system. In that case "restoration" is provided + * by your app passing the same `appUserId` used to purchase originally. + * + * - Note: This may force your users to enter the App Store password so should only be performed on request of + * the user. Typically with a button in settings or near your purchase UI. Use syncPurchasesWithCompletionBlock + * if you need to restore transactions programmatically. + */ + @objc func restoreTransactions(completionBlock completion: ReceivePurchaserInfoBlock? = nil) { + purchasesOrchestrator.restoreTransactions(completion: completion) + } + + /** + * Computes whether or not a user is eligible for the introductory pricing period of a given product. + * You should use this method to determine whether or not you show the user the normal product price or + * the introductory price. This also applies to trials (trials are considered a type of introductory pricing). + * + * - Note: Subscription groups are automatically collected for determining eligibility. If RevenueCat can't + * definitively compute the eligibilty, most likely because of missing group information, it will return + * `RCIntroEligibilityStatusUnknown`. The best course of action on unknown status is to display the non-intro + * pricing, to not create a misleading situation. To avoid this, make sure you are testing with the latest + * version of iOS so that the subscription group can be collected by the SDK. + * + * - Parameter productIdentifiers: Array of product identifiers for which you want to compute eligibility + * - Parameter receiveEligibility: A block that receives a dictionary of product_id -> `RCIntroEligibility`. + */ + @objc + func checkTrialOrIntroductoryPriceEligibility(_ productIdentifiers: [String], + completionBlock receiveEligibility: @escaping ReceiveIntroEligibilityBlock) { + receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { maybeData in + if #available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *), + let data = maybeData { + self.modernEligibilityHandler(maybeReceiptData: data, + productIdentifiers: productIdentifiers, + completionBlock: receiveEligibility) + } else { + self.backend.getIntroEligibility(appUserID: self.appUserID, + receiptData: maybeData ?? Data(), + productIdentifiers: productIdentifiers) { result, maybeError in + if let error = maybeError { + Logger.error(String(format: "Unable to getIntroEligibilityForAppUserID: %@", + error.localizedDescription)) + } + self.operationDispatcher.dispatchOnMainThread { + receiveEligibility(result) + } + } + } + } + } + + /** + * Invalidates the cache for purchaser information. + * + * Most apps will not need to use this method; invalidating the cache can leave your app in an invalid state. + * Refer to https://docs.revenuecat.com/docs/purchaserinfo#section-get-user-information for more information on + * using the cache properly. + * + * This is useful for cases where purchaser information might have been updated outside of the app, like if a + * promotional subscription is granted through the RevenueCat dashboard. + */ + @objc func invalidatePurchaserInfoCache() { + purchaserInfoManager.clearPurchaserInfoCache(forAppUserID: appUserID) + } + + #if os(iOS) + /** + * Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect. + */ + @available(iOS 14.0, *) + @objc func presentCodeRedemptionSheet() { + storeKitWrapper.presentCodeRedemptionSheet() + } + #endif + + /** + * Use this function to retrieve the `SKPaymentDiscount` for a given `SKProduct`. + * + * - Parameter discount: The `SKProductDiscount` to apply to the product. + * - Parameter product: The `SKProduct` the user intends to purchase. + * - Parameter completion: A completion block that is called when the `SKPaymentDiscount` is returned. + * If it was not successful, there will be an `NSError`. + */ + @available(iOS 12.2, macOS 10.14.4, macCatalyst 13.0, tvOS 12.2, watchOS 6.2, *) + @objc(paymentDiscountForProductDiscount:product:completion:) + func paymentDiscount(forProductDiscount discount: SKProductDiscount, + product: SKProduct, + completion: @escaping PaymentDiscountBlock) { + purchasesOrchestrator.paymentDiscount(forProductDiscount: discount, product: product, completion: completion) + } + + @available(iOS 12.0, macOS 10.14, macCatalyst 13.0, tvOS 12.0, watchOS 6.2, *) + private func modernEligibilityHandler(maybeReceiptData data: Data, + productIdentifiers: [String], + completionBlock receiveEligibility: @escaping ReceiveIntroEligibilityBlock) { + introEligibilityCalculator + .checkTrialOrIntroductoryPriceEligibility(with: data, + productIdentifiers: Set(productIdentifiers)) { receivedEligibility, maybeError in + if let error = maybeError { + Logger.error(String(format: Strings.receipt.parse_receipt_locally_error, + error.localizedDescription)) + self.backend.getIntroEligibility(appUserID: self.appUserID, + receiptData: data, + productIdentifiers: productIdentifiers) { result, maybeAnotherError in + if let intoEligibilityError = maybeAnotherError { + Logger.error(String(format: "Unable to getIntroEligibilityForAppUserID: %@", + intoEligibilityError.localizedDescription)) + } + self.operationDispatcher.dispatchOnMainThread { + receiveEligibility(result) + } + } + } else { + var convertedEligibility: [String: IntroEligibility] = [:] + for (key, value) in receivedEligibility { + do { + let introEligibility = try IntroEligibility(eligibilityStatusCode: value) + convertedEligibility[key] = introEligibility + } catch { + Logger.error(String(format: "Unable to create an RCIntroEligibility: %@", + error.localizedDescription)) + } + } + self.operationDispatcher.dispatchOnMainThread { + receiveEligibility(convertedEligibility) + } + } + } + } + + private func purchase(product: SKProduct, + payment: SKMutablePayment, + presentedOfferingIdentifier: String?, + completion: @escaping PurchaseCompletedBlock) { + purchasesOrchestrator.purchase(product: product, + payment: payment, + presentedOfferingIdentifier: presentedOfferingIdentifier, + completion: completion) + } + +} + +// MARK: Configuring Purchases +extension Purchases { + + /** + * Configures an instance of the Purchases SDK with a specified API key. The instance will be set as a singleton. + * You should access the singleton instance using [RCPurchases sharedPurchases] + * + * - Note: Use this initializer if your app does not have an account system. + * `RCPurchases` will generate a unique identifier for the current device and persist it to `NSUserDefaults`. + * This also affects the behavior of `restoreTransactionsWithCompletionBlock`. + * + * - Parameter apiKey: The API Key generated for your app from https://app.revenuecat.com/ + * + * - Returns: An instantiated `RCPurchases` object that has been set as a singleton. + */ + @objc(configureWithAPIKey:) + @discardableResult static func configure(apiKey: String) -> Purchases { + configure(apiKey: apiKey, appUserID: nil) + } + + /** + * Configures an instance of the Purchases SDK with a specified API key and app user ID. + * The instance will be set as a singleton. + * You should access the singleton instance using [RCPurchases sharedPurchases] + * + * - Note: Best practice is to use a salted hash of your unique app user ids. + * + * - Warning: Use this initializer if you have your own user identifiers that you manage. + * + * - Parameter apiKey: The API Key generated for your app from https://app.revenuecat.com/ + * + * - Parameter appUserID: The unique app user id for this user. This user id will allow users to share their + * purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. + * + * - Returns: An instantiated `RCPurchases` object that has been set as a singleton. + */ + @objc(configureWithAPIKey:appUserID:) + @discardableResult static func configure(apiKey: String, appUserID: String?) -> Purchases { + configure(apiKey: apiKey, appUserID: appUserID, observerMode: false) + } + + /** + * Configures an instance of the Purchases SDK with a custom userDefaults. Use this constructor if you want to + * sync status across a shared container, such as between a host app and an extension. The instance of the + * Purchases SDK will be set as a singleton. + * You should access the singleton instance using [RCPurchases sharedPurchases] + * + * - Parameter apiKey: The API Key generated for your app from https://app.revenuecat.com/ + * + * - Parameter appUserID: The unique app user id for this user. This user id will allow users to share their + * purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. + * + * - Parameter observerMode: Set this to TRUE if you have your own IAP implementation and want to use only + * RevenueCat's backend. Default is FALSE. + * + * - Returns: An instantiated `RCPurchases` object that has been set as a singleton. + */ + @objc(configureWithAPIKey:appUserID:observerMode:) + @discardableResult static func configure(apiKey: String, appUserID: String?, observerMode: Bool) -> Purchases { + configure(apiKey: apiKey, appUserID: appUserID, observerMode: observerMode, userDefaults: nil) + } + + /** + * Configures an instance of the Purchases SDK with a custom userDefaults. Use this constructor if you want to + * sync status across a shared container, such as between a host app and an extension. The instance of the + * Purchases SDK will be set as a singleton. + * You should access the singleton instance using [RCPurchases sharedPurchases] + * + * - Parameter apiKey: The API Key generated for your app from https://app.revenuecat.com/ + * + * - Parameter appUserID: The unique app user id for this user. This user id will allow users to share their + * purchases and subscriptions across devices. Pass nil if you want `RCPurchases` to generate this for you. + * + * - Parameter observerMode: Set this to TRUE if you have your own IAP implementation and want to use only + * RevenueCat's backend. Default is FALSE. + * + * - Parameter userDefaults: Custom userDefaults to use + * + * - Returns: An instantiated `RCPurchases` object that has been set as a singleton. + */ + @objc(configureWithAPIKey:appUserID:observerMode:userDefaults:) + @discardableResult static func configure(apiKey: String, + appUserID: String?, + observerMode: Bool, + userDefaults: UserDefaults?) -> Purchases { + configure(apiKey: apiKey, + appUserID: appUserID, + observerMode: observerMode, + userDefaults: userDefaults, + platformFlavor: nil, + platformFlavorVersion: nil) + } + + public static func configure(apiKey: String, + appUserID: String?, + observerMode: Bool, + userDefaults: UserDefaults?, + platformFlavor: String?, + platformFlavorVersion: String?) -> Purchases { + let purchases = Purchases(apiKey: apiKey, + appUserID: appUserID, + userDefaults: userDefaults, + observerMode: observerMode, + platformFlavor: platformFlavor, + platformFlavorVersion: platformFlavorVersion) + setDefaultInstance(purchases) + return purchases + } + +} + +// MARK: Delegate implementation +extension Purchases: PurchaserInfoManagerDelegate { + + public func purchaserInfoManagerDidReceiveUpdated(purchaserInfo: PurchaserInfo) { + delegate?.purchases?(self, didReceiveUpdated: purchaserInfo) + } + +} + +extension Purchases: PurchasesOrchestratorDelegate { + + /** + * Called when a user initiates a promotional in-app purchase from the App Store. + * + * If your app is able to handle a purchase at the current time, run the deferment block in this method. + * + * If the app is not in a state to make a purchase: cache the defermentBlock, then call the defermentBlock + * when the app is ready to make the promotional purchase. + * + * If the purchase should never be made, you don't need to ever call the defermentBlock and `Purchases` + * will not proceed with promotional purchases. + * + * - Parameter product: `SKProduct` the product that was selected from the app store + */ + @objc + public func shouldPurchasePromoProduct(_ product: SKProduct, + defermentBlock: @escaping DeferredPromotionalPurchaseBlock) { + guard let delegate = delegate else { + return + } + + delegate.purchases?(self, shouldPurchasePromoProduct: product, defermentBlock: defermentBlock) + } + +} + +// MARK: Private +private extension Purchases { + + @objc func applicationDidBecomeActive(notification: Notification) { + Logger.debug(Strings.configure.application_active) + updateAllCachesIfNeeded() + syncSubscriberAttributesIfNeeded() + postAppleSearchAddsAttributionCollectionIfNeeded() + } + + @objc func applicationWillResignActive(notification: Notification) { + syncSubscriberAttributesIfNeeded() + } + + func subscribeToAppStateNotifications() { + notificationCenter.addObserver(self, + selector: #selector(applicationDidBecomeActive(notification:)), + name: SystemInfo.applicationDidBecomeActiveNotification, object: nil) + + notificationCenter.addObserver(self, + selector: #selector(applicationWillResignActive(notification:)), + name: SystemInfo.applicationWillResignActiveNotification, object: nil) + } + + func syncSubscriberAttributesIfNeeded() { + operationDispatcher.dispatchOnWorkerThread { + self.subscriberAttributesManager.syncAttributesForAllUsers(currentAppUserID: self.appUserID) + } + } + + func updateAllCachesIfNeeded() { + systemInfo.isApplicationBackgrounded { isAppBackgrounded in + self.purchaserInfoManager.fetchAndCachePurchaserInfoIfStale(appUserID: self.appUserID, + isAppBackgrounded: isAppBackgrounded, + completion: nil) + guard self.deviceCache.isOfferingsCacheStale(isAppBackgrounded: isAppBackgrounded) else { + return + } + + Logger.debug("Offerings cache is stale, updating caches") + self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, + isAppBackgrounded: isAppBackgrounded, + completion: nil) + } + } + + func updateAllCaches(completion: ReceivePurchaserInfoBlock?) { + systemInfo.isApplicationBackgrounded { isAppBackgrounded in + self.purchaserInfoManager.fetchAndCachePurchaserInfo(appUserID: self.appUserID, + isAppBackgrounded: isAppBackgrounded, + completion: completion) + self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, + isAppBackgrounded: isAppBackgrounded, + completion: nil) + } + } + +} diff --git a/PurchasesCoreSwift/Public/PurchasesDelegate.swift b/PurchasesCoreSwift/Public/PurchasesDelegate.swift new file mode 100644 index 0000000000..1fa37fd515 --- /dev/null +++ b/PurchasesCoreSwift/Public/PurchasesDelegate.swift @@ -0,0 +1,49 @@ +// +// 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 +// +// PurchasesDelegate.swift +// +// Created by Joshua Liebowitz on 8/18/21. + +import Foundation +import StoreKit + +/** + * Delegate for `RCPurchases` responsible for handling updating your app's state in response to updated purchaser info + * or promotional product purchases. + * + * @note Delegate methods can be called at any time after the `delegate` is set, not just in response to + * `purchaserInfo:` calls. Ensure your app is capable of handling these calls at anytime if `delegate` is set. + */ +@objc(RCPurchasesDelegate) public protocol PurchasesDelegate: NSObjectProtocol { + + /** + * Called whenever `Purchases` receives updated purchaser info. This may happen periodically + * throughout the life of the app if new information becomes available (e.g. UIApplicationDidBecomeActive).* + * @param purchases Related `Purchases` object + * @param purchaserInfo Updated `PurchaserInfo` + */ + @objc(purchases:didReceiveUpdatedPurchaserInfo:) + optional func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) + + /** + * Called when a user initiates a promotional in-app purchase from the App Store. + * If your app is able to handle a purchase at the current time, run the deferment block in this method. + * If the app is not in a state to make a purchase: cache the defermentBlock, + * then call the defermentBlock when the app is ready to make the promotional purchase. + * If the purchase should never be made, you don't need to ever call the defermentBlock and + * `RCPurchases` will not proceed with promotional purchases. + + * @param product `SKProduct` the product that was selected from the app store + */ + @objc optional func purchases(_ purchases: Purchases, + shouldPurchasePromoProduct product: SKProduct, + defermentBlock makeDeferredPurchase: @escaping DeferredPromotionalPurchaseBlock) + +} diff --git a/PurchasesCoreSwift/PurchasesCoreSwift.h b/PurchasesCoreSwift/PurchasesCoreSwift.h index 1522351fd6..a0874c8432 100644 --- a/PurchasesCoreSwift/PurchasesCoreSwift.h +++ b/PurchasesCoreSwift/PurchasesCoreSwift.h @@ -16,4 +16,55 @@ FOUNDATION_EXPORT const unsigned char PurchasesCoreSwiftVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import +@class RCPurchaserInfo, RCIntroEligibility, RCOfferings; +@class SKPaymentTransaction, SKProduct, SKPaymentDiscount; +NS_ASSUME_NONNULL_BEGIN +/** + Completion block for calls that send back a `PurchaserInfo` + */ +typedef void (^RCReceivePurchaserInfoBlock)(RCPurchaserInfo * _Nullable, NSError * _Nullable) +NS_SWIFT_UNAVAILABLE("Use ReceivePurchaserInfoBlock instead."); + + +/** + Completion block for `-[RCPurchases checkTrialOrIntroductoryPriceEligibility:completionBlock:]` + */ +typedef void (^RCReceiveIntroEligibilityBlock)(NSDictionary *) +NS_SWIFT_UNAVAILABLE("Use ReceiveIntroEligibilityBlock instead."); + +/** + Completion block for `-[RCPurchases offeringsWithCompletionBlock:]` + */ +typedef void (^RCReceiveOfferingsBlock)(RCOfferings * _Nullable, NSError * _Nullable) +NS_SWIFT_UNAVAILABLE("Use ReceiveOfferingsBlock instead."); + +/** + Completion block for `-[RCPurchases productsWithIdentifiers:completionBlock:]` + */ +typedef void (^RCReceiveProductsBlock)(NSArray *) +NS_SWIFT_UNAVAILABLE("Use ReceiveProductsBlock instead."); + +/** + Completion block for `-[RCPurchases purchaseProduct:withCompletionBlock:]` + */ +typedef void (^RCPurchaseCompletedBlock)(SKPaymentTransaction * _Nullable, + RCPurchaserInfo * _Nullable, + NSError * _Nullable, + BOOL userCancelled) +NS_SWIFT_UNAVAILABLE("Use PurchaseCompletedBlock instead."); + +/** + Deferred block for `purchases:shouldPurchasePromoProduct:defermentBlock:` + */ +typedef void (^RCDeferredPromotionalPurchaseBlock)(RCPurchaseCompletedBlock) +NS_SWIFT_UNAVAILABLE("Use DeferredPromotionalPurchaseBlock instead."); + +/** + * Deferred block for `-[RCPurchases paymentDiscountForProductDiscount:product:completion:]` + */ +API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), macCatalyst(13.0), tvos(12.2)) +typedef void (^RCPaymentDiscountBlock)(SKPaymentDiscount * _Nullable, NSError * _Nullable) +NS_SWIFT_UNAVAILABLE("Use PaymentDiscountBlock instead."); + +NS_ASSUME_NONNULL_END diff --git a/PurchasesCoreSwift/Purchasing/OfferingsManager.swift b/PurchasesCoreSwift/Purchasing/OfferingsManager.swift index cc7b5f3462..cefdee0596 100644 --- a/PurchasesCoreSwift/Purchasing/OfferingsManager.swift +++ b/PurchasesCoreSwift/Purchasing/OfferingsManager.swift @@ -14,8 +14,6 @@ import Foundation import StoreKit -public typealias ReceiveOfferingsBlock = (Offerings?, Error?) -> Void - // TODO (post-migration): Make all the things internal again. @objc(RCOfferingsManager) public class OfferingsManager: NSObject { diff --git a/PurchasesCoreSwift/Purchasing/PurchasesOrchestrator.swift b/PurchasesCoreSwift/Purchasing/PurchasesOrchestrator.swift index 34adecf000..9844ddf01c 100644 --- a/PurchasesCoreSwift/Purchasing/PurchasesOrchestrator.swift +++ b/PurchasesCoreSwift/Purchasing/PurchasesOrchestrator.swift @@ -14,12 +14,9 @@ import Foundation import StoreKit -public typealias PurchaseCompletedBlock = (SKPaymentTransaction?, PurchaserInfo?, Error?, Bool) -> Void -public typealias RCDeferredPromotionalPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void - @objc(RCPurchasesOrchestratorDelegate) public protocol PurchasesOrchestratorDelegate { - func shouldPurchasePromoProduct(_ product: SKProduct, defermentBlock: @escaping RCDeferredPromotionalPurchaseBlock) + func shouldPurchasePromoProduct(_ product: SKProduct, defermentBlock: @escaping DeferredPromotionalPurchaseBlock) } @@ -89,7 +86,7 @@ public typealias RCDeferredPromotionalPurchaseBlock = (@escaping PurchaseComplet syncPurchases(receiptRefreshPolicy: .always, isRestore: true, maybeCompletion: maybeCompletion) } - @objc public func syncPurchases(completion maybeCompletion: ((PurchaserInfo?, Error?) -> Void)?) { + @objc public func syncPurchases(completion maybeCompletion: ((PurchaserInfo?, Error?) -> Void)? = nil) { syncPurchases(receiptRefreshPolicy: .never, isRestore: allowSharingAppStoreAccount, maybeCompletion: maybeCompletion) diff --git a/PurchasesCoreSwiftTests/FoundationExtensions/NSData+RCExtensionsTests.swift b/PurchasesCoreSwiftTests/FoundationExtensions/NSData+RCExtensionsTests.swift index 48510c56d6..34640b5542 100644 --- a/PurchasesCoreSwiftTests/FoundationExtensions/NSData+RCExtensionsTests.swift +++ b/PurchasesCoreSwiftTests/FoundationExtensions/NSData+RCExtensionsTests.swift @@ -6,7 +6,6 @@ import Foundation import XCTest import Nimble -import Purchases @testable import PurchasesCoreSwift diff --git a/PurchasesCoreSwiftTests/Misc/SystemInfoTests.swift b/PurchasesCoreSwiftTests/Misc/SystemInfoTests.swift index 6792e2b3c1..135923e4c9 100644 --- a/PurchasesCoreSwiftTests/Misc/SystemInfoTests.swift +++ b/PurchasesCoreSwiftTests/Misc/SystemInfoTests.swift @@ -1,7 +1,7 @@ import XCTest import Nimble -import Purchases +import PurchasesCoreSwift class SystemInfoTests: XCTestCase { func testproxyURL() { diff --git a/PurchasesCoreSwiftTests/Networking/ETagManagerTests.swift b/PurchasesCoreSwiftTests/Networking/ETagManagerTests.swift index 80f0c58a3c..b3ecc6478a 100644 --- a/PurchasesCoreSwiftTests/Networking/ETagManagerTests.swift +++ b/PurchasesCoreSwiftTests/Networking/ETagManagerTests.swift @@ -2,7 +2,6 @@ import XCTest import Nimble import StoreKit -import Purchases @testable import PurchasesCoreSwift class ETagManagerTests: XCTestCase { diff --git a/PurchasesCoreSwiftTests/Networking/HTTPClientTests.swift b/PurchasesCoreSwiftTests/Networking/HTTPClientTests.swift index 15b5eb2d98..9406288e88 100644 --- a/PurchasesCoreSwiftTests/Networking/HTTPClientTests.swift +++ b/PurchasesCoreSwiftTests/Networking/HTTPClientTests.swift @@ -111,7 +111,7 @@ class HTTPClientTests: XCTestCase { let path = "/a_random_path" var headerPresent = false - stub(condition: hasHeaderNamed("X-Version", value: Purchases.frameworkVersion())) { request in + stub(condition: hasHeaderNamed("X-Version", value: Purchases.frameworkVersion)) { request in headerPresent = true return HTTPStubsResponse(data: Data.init(), statusCode: 200, headers: nil) } diff --git a/PurchasesCoreSwiftTests/Purchasing/OfferingsTests.swift b/PurchasesCoreSwiftTests/Purchasing/OfferingsTests.swift index a3c53c1484..2ad9185902 100644 --- a/PurchasesCoreSwiftTests/Purchasing/OfferingsTests.swift +++ b/PurchasesCoreSwiftTests/Purchasing/OfferingsTests.swift @@ -7,8 +7,9 @@ // import Foundation -import XCTest import Nimble +import StoreKit +import XCTest import Purchases @testable import PurchasesCoreSwift diff --git a/PurchasesCoreSwiftTests/Purchasing/StoreKitWrapperTests.swift b/PurchasesCoreSwiftTests/Purchasing/StoreKitWrapperTests.swift index b839de9059..ba05250d35 100644 --- a/PurchasesCoreSwiftTests/Purchasing/StoreKitWrapperTests.swift +++ b/PurchasesCoreSwiftTests/Purchasing/StoreKitWrapperTests.swift @@ -7,8 +7,9 @@ // import Foundation -import XCTest import Nimble +import StoreKit +import XCTest @testable import PurchasesCoreSwift diff --git a/PurchasesTests/Misc/ISOPeriodFormatterTests.swift b/PurchasesTests/Misc/ISOPeriodFormatterTests.swift index ddf3d07267..e63b8f16e7 100644 --- a/PurchasesTests/Misc/ISOPeriodFormatterTests.swift +++ b/PurchasesTests/Misc/ISOPeriodFormatterTests.swift @@ -7,13 +7,12 @@ // import Foundation -import XCTest import Nimble +import StoreKit +import XCTest -import Purchases @testable import PurchasesCoreSwift - class ISOPeriodFormatterTests: XCTestCase { func testStringFromProductSubscriptionPeriodDay() { diff --git a/PurchasesTests/Mocks/MockNotificationCenter.swift b/PurchasesTests/Mocks/MockNotificationCenter.swift index aa73eed925..0016c849cc 100644 --- a/PurchasesTests/Mocks/MockNotificationCenter.swift +++ b/PurchasesTests/Mocks/MockNotificationCenter.swift @@ -3,6 +3,8 @@ // Copyright (c) 2020 Purchases. All rights reserved. // +import Foundation + class MockNotificationCenter: NotificationCenter { typealias AddObserverTuple = (observer: AnyObject, diff --git a/PurchasesTests/Mocks/MockOfferingsFactory.swift b/PurchasesTests/Mocks/MockOfferingsFactory.swift index 9286a2526a..a451011f5f 100644 --- a/PurchasesTests/Mocks/MockOfferingsFactory.swift +++ b/PurchasesTests/Mocks/MockOfferingsFactory.swift @@ -3,6 +3,7 @@ // Copyright (c) 2020 Purchases. All rights reserved. // @testable import PurchasesCoreSwift +import StoreKit class MockOfferingsFactory: OfferingsFactory { diff --git a/PurchasesTests/Mocks/MockPaymentDiscount.swift b/PurchasesTests/Mocks/MockPaymentDiscount.swift index c1237a4387..be86706bb8 100644 --- a/PurchasesTests/Mocks/MockPaymentDiscount.swift +++ b/PurchasesTests/Mocks/MockPaymentDiscount.swift @@ -7,6 +7,7 @@ // import Foundation +import StoreKit @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) class MockPaymentDiscount: SKPaymentDiscount { diff --git a/PurchasesTests/Mocks/MockPurchasesDelegate.swift b/PurchasesTests/Mocks/MockPurchasesDelegate.swift index c4d6b925f0..8e8a4f94fa 100644 --- a/PurchasesTests/Mocks/MockPurchasesDelegate.swift +++ b/PurchasesTests/Mocks/MockPurchasesDelegate.swift @@ -3,22 +3,29 @@ // Copyright (c) 2020 Purchases. All rights reserved. // -class MockPurchasesDelegate: NSObject, PurchasesDelegate { +import Foundation +import StoreKit +@testable import PurchasesCoreSwift + +public class MockPurchasesDelegate: NSObject, PurchasesDelegate { + var purchaserInfo: PurchaserInfo? var purchaserInfoReceivedCount = 0 - func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) { + public func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) { purchaserInfoReceivedCount += 1 self.purchaserInfo = purchaserInfo } var promoProduct: SKProduct? - var makeDeferredPurchase: RCDeferredPromotionalPurchaseBlock? + var makeDeferredPurchase: DeferredPromotionalPurchaseBlock? - func purchases(_ purchases: Purchases, - shouldPurchasePromoProduct product: SKProduct, - defermentBlock makeDeferredPurchase: @escaping RCDeferredPromotionalPurchaseBlock) { + + public func purchases(_ purchases: Purchases, + shouldPurchasePromoProduct product: SKProduct, + defermentBlock makeDeferredPurchase: @escaping (@escaping PurchaseCompletedBlock) -> Void) { promoProduct = product self.makeDeferredPurchase = makeDeferredPurchase } + } diff --git a/PurchasesTests/Mocks/MockSKDiscount.swift b/PurchasesTests/Mocks/MockSKDiscount.swift index 55c7b63652..597c3534a4 100644 --- a/PurchasesTests/Mocks/MockSKDiscount.swift +++ b/PurchasesTests/Mocks/MockSKDiscount.swift @@ -4,6 +4,7 @@ // import Foundation +import StoreKit @available(iOS 11.2, tvOS 11.2, macOS 10.13.2, *) class MockDiscount: SKProductDiscount { diff --git a/PurchasesTests/Mocks/MockSKProduct.swift b/PurchasesTests/Mocks/MockSKProduct.swift index 00c9a0819f..b8b2164e0e 100644 --- a/PurchasesTests/Mocks/MockSKProduct.swift +++ b/PurchasesTests/Mocks/MockSKProduct.swift @@ -2,6 +2,8 @@ // Created by RevenueCat on 3/2/20. // Copyright (c) 2020 Purchases. All rights reserved. // + +import PurchasesCoreSwift import StoreKit class MockSKProduct: SKProduct { diff --git a/PurchasesTests/Mocks/MockStoreKitWrapper.swift b/PurchasesTests/Mocks/MockStoreKitWrapper.swift index 8c6f1f03e5..7cad322a53 100644 --- a/PurchasesTests/Mocks/MockStoreKitWrapper.swift +++ b/PurchasesTests/Mocks/MockStoreKitWrapper.swift @@ -4,6 +4,7 @@ // @testable import PurchasesCoreSwift +import StoreKit class MockStoreKitWrapper: StoreKitWrapper { var payment: SKPayment? diff --git a/PurchasesTests/Mocks/MockTransaction.swift b/PurchasesTests/Mocks/MockTransaction.swift index ab4ce2d4b5..fdf06218c1 100644 --- a/PurchasesTests/Mocks/MockTransaction.swift +++ b/PurchasesTests/Mocks/MockTransaction.swift @@ -3,6 +3,8 @@ // Copyright (c) 2020 Purchases. All rights reserved. // +import StoreKit + class MockTransaction: SKPaymentTransaction { var mockPayment: SKPayment? diff --git a/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift b/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift index 6bfb40df7d..4ba2cb8b3d 100644 --- a/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift +++ b/PurchasesTests/Mocks/SKProductSubscriptionDurationExtensions.swift @@ -7,6 +7,7 @@ // import Foundation +import StoreKit @available(iOS 11.2, macOS 10.13.2, tvOS 11.2, *) extension SKProductSubscriptionPeriod { diff --git a/PurchasesTests/PurchasesTests-Bridging-Header.h b/PurchasesTests/PurchasesTests-Bridging-Header.h index 59d133f29b..32430dd2eb 100644 --- a/PurchasesTests/PurchasesTests-Bridging-Header.h +++ b/PurchasesTests/PurchasesTests-Bridging-Header.h @@ -1,9 +1,5 @@ // // Use this file to import your target's public headers that you would like to expose to Swift. // -@import PurchasesCoreSwift; -#include -#include - -#include "RCObjC.h" +#import "RCObjC.h" diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index a6b7c677e1..605c9e02af 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -3,10 +3,10 @@ // Copyright © 2019 RevenueCat. All rights reserved. // -import XCTest import Nimble +import StoreKit +import XCTest -import Purchases @testable import PurchasesCoreSwift class PurchasesTests: XCTestCase { @@ -59,10 +59,9 @@ class PurchasesTests: XCTestCase { } override func tearDown() { + Purchases.clearSingleton() deviceCache = nil - purchases?.delegate = nil purchases = nil - Purchases.setDefaultInstance(nil) UserDefaults().removePersistentDomain(forName: "TestDefaults") } @@ -330,7 +329,18 @@ class PurchasesTests: XCTestCase { } func testUsingSharedInstanceWithoutInitializingRaisesException() { - expectToThrowException(.parameterAssert) { _ = Purchases.shared } + #if arch(arm64) + let assertionHappened = expectation(description: "Assertion happened") + Purchases.notConfiguredAssertionFunction = { + assertionHappened.fulfill() + } + let purchases = Purchases.shared + wait(for: [assertionHappened], timeout: TimeInterval(1)) + + #else + expect(Purchases.shared).to(throwAssertion()) + + #endif setupPurchases() expectToNotThrowException { _ = Purchases.shared } } @@ -543,7 +553,7 @@ class PurchasesTests: XCTestCase { func testDelegateIsNotCalledIfBlockPassed() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -565,7 +575,7 @@ class PurchasesTests: XCTestCase { setupPurchases() var products: [SKProduct]? let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.products(productIdentifiers) { (newProducts) in + purchases!.products(identifiers: productIdentifiers) { (newProducts) in products = newProducts } @@ -581,7 +591,7 @@ class PurchasesTests: XCTestCase { func testAddsPaymentToWrapper() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -592,7 +602,7 @@ class PurchasesTests: XCTestCase { func testPurchaseProductCachesProduct() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -604,7 +614,7 @@ class PurchasesTests: XCTestCase { setupPurchases() var completionCalled = false mockProductsManager.resetMock() - self.purchases.products([]) { _ in + self.purchases.products(identifiers: []) { _ in completionCalled = true } expect(completionCalled).toEventually(beTrue()) @@ -614,7 +624,7 @@ class PurchasesTests: XCTestCase { func testTransitioningToPurchasing() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -630,7 +640,7 @@ class PurchasesTests: XCTestCase { func testTransitioningToPurchasedSendsToBackend() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -650,7 +660,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsAsRestoreWhenAnon() { setupAnonPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -671,7 +681,7 @@ class PurchasesTests: XCTestCase { setupAnonPurchases() self.purchases.allowSharingAppStoreAccount = false let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -692,7 +702,7 @@ class PurchasesTests: XCTestCase { setupPurchases() self.purchases.allowSharingAppStoreAccount = true let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -712,7 +722,7 @@ class PurchasesTests: XCTestCase { func testFinishesTransactionsIfSentToBackendCorrectly() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -735,7 +745,7 @@ class PurchasesTests: XCTestCase { setupPurchases() self.purchases?.finishTransactions = false let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -757,9 +767,9 @@ class PurchasesTests: XCTestCase { func testSendsProductInfoIfProductIsCached() { setupPurchases() let productIdentifiers = ["com.product.id1", "com.product.id2"] - purchases!.products(productIdentifiers) { (newProducts) in + purchases!.products(identifiers: productIdentifiers) { (newProducts) in let product = newProducts[0]; - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -840,7 +850,7 @@ class PurchasesTests: XCTestCase { func testAfterSendingDoesntFinishTransactionIfBackendError() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -858,7 +868,7 @@ class PurchasesTests: XCTestCase { func testAfterSendingFinishesFromBackendErrorIfAppropriate() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -877,7 +887,7 @@ class PurchasesTests: XCTestCase { func testNotifiesIfTransactionFailsFromBackend() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -897,7 +907,7 @@ class PurchasesTests: XCTestCase { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") var receivedError: Error? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error } @@ -942,7 +952,7 @@ class PurchasesTests: XCTestCase { self.backend.overridePurchaserInfo = purchaserInfoBeforePurchase self.backend.postReceiptPurchaserInfo = purchaserInfoAfterPurchase - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in purchaserInfo = info receivedError = error receivedUserCancelled = userCancelled @@ -966,7 +976,7 @@ class PurchasesTests: XCTestCase { var callCount = 0 - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in callCount += 1 } @@ -990,7 +1000,7 @@ class PurchasesTests: XCTestCase { var callCount = 0 - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in callCount += 1 } @@ -1011,7 +1021,7 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "com.product.id1") // First one "works" - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } var receivedInfo: PurchaserInfo? @@ -1019,7 +1029,7 @@ class PurchasesTests: XCTestCase { var receivedUserCancelled: Bool? // Second one issues an error - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedInfo = info receivedError = error as NSError? receivedUserCancelled = userCancelled @@ -1193,13 +1203,13 @@ class PurchasesTests: XCTestCase { func testRestoringPurchasesSetsIsRestore() { setupPurchases() - purchases!.restoreTransactions(nil) + purchases!.restoreTransactions() expect(self.backend.postedIsRestore!).to(beTrue()) } func testRestoringPurchasesSetsIsRestoreForAnon() { setupAnonPurchases() - purchases!.restoreTransactions(nil) + purchases!.restoreTransactions() expect(self.backend.postedIsRestore!).to(beTrue()) } @@ -1241,7 +1251,7 @@ class PurchasesTests: XCTestCase { func testSyncPurchasesPostsTheReceipt() { setupPurchases() - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postReceiptDataCalled).to(beTrue()) } @@ -1265,7 +1275,7 @@ class PurchasesTests: XCTestCase { mockReceiptParser.stubbedReceiptHasTransactionsResult = false setupPurchases() - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postReceiptDataCalled) == false } @@ -1274,7 +1284,7 @@ class PurchasesTests: XCTestCase { mockReceiptParser.stubbedReceiptHasTransactionsResult = false setupPurchases() - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postReceiptDataCalled) == true } @@ -1299,7 +1309,7 @@ class PurchasesTests: XCTestCase { mockReceiptParser.stubbedReceiptHasTransactionsResult = true setupPurchases() - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postReceiptDataCalled) == true } @@ -1308,7 +1318,7 @@ class PurchasesTests: XCTestCase { mockReceiptParser.stubbedReceiptHasTransactionsResult = true setupPurchases() - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postReceiptDataCalled) == true } @@ -1316,7 +1326,7 @@ class PurchasesTests: XCTestCase { func testSyncPurchasesDoesntRefreshTheReceiptIfNotEmpty() { setupPurchases() self.receiptFetcher.shouldReturnReceipt = true - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.receiptFetcher.receiptDataTimesCalled) == 1 expect(self.requestFetcher.refreshReceiptCalled) == false @@ -1325,7 +1335,7 @@ class PurchasesTests: XCTestCase { func testSyncPurchasesDoesntRefreshTheReceiptIfEmpty() { setupPurchases() self.receiptFetcher.shouldReturnReceipt = false - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.receiptFetcher.receiptDataTimesCalled) == 1 expect(self.requestFetcher.refreshReceiptCalled) == false @@ -1335,11 +1345,11 @@ class PurchasesTests: XCTestCase { setupPurchases() purchases.allowSharingAppStoreAccount = false - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postedIsRestore!) == false purchases.allowSharingAppStoreAccount = true - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postedIsRestore!) == true } @@ -1347,11 +1357,11 @@ class PurchasesTests: XCTestCase { setupAnonPurchases() purchases.allowSharingAppStoreAccount = false - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postedIsRestore!) == false purchases.allowSharingAppStoreAccount = true - purchases!.syncPurchases() + purchases!.syncPurchases(completionBlock: nil) expect(self.backend.postedIsRestore!) == true } @@ -1464,16 +1474,25 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "mock_product") let payment = SKPayment.init(product: product) - _ = storeKitWrapper.delegate?.storeKitWrapper(storeKitWrapper, - shouldAddStorePayment: payment, - for: product) + guard let storeKitWrapperDelegate = storeKitWrapper.delegate else { + fail("storeKitWrapperDelegate nil") + return + } + + _ = storeKitWrapperDelegate.storeKitWrapper(storeKitWrapper, + shouldAddStorePayment: payment, + for: product) expect(self.purchasesDelegate.makeDeferredPurchase).toNot(beNil()) expect(self.storeKitWrapper.payment).to(beNil()) - self.purchasesDelegate.makeDeferredPurchase! { (_, _, _, _) in + guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { + fail("makeDeferredPurchase should have been nonNil") + return + } + makeDeferredPurchase { (_, _, _, _) in } expect(self.storeKitWrapper.payment).to(be(payment)) @@ -1562,7 +1581,7 @@ class PurchasesTests: XCTestCase { ]]); let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -1769,7 +1788,7 @@ class PurchasesTests: XCTestCase { mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) self.purchases?.offerings { (newOfferings, _) in let product = newOfferings!["base"]!.monthly!.product; - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -1798,7 +1817,7 @@ class PurchasesTests: XCTestCase { func testAddAttributionAlwaysAddsAdIdsEmptyDict() { setupPurchases() - Purchases.addAttributionData([:], from: AttributionNetwork.adjust) + Purchases.addAttributionData([:], fromNetwork: AttributionNetwork.adjust) let attributionData = self.subscriberAttributesManager.invokedConvertAttributionDataAndSetParameters?.attributionData expect(attributionData?.count) == 2 @@ -1810,7 +1829,7 @@ class PurchasesTests: XCTestCase { setupPurchases() let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - Purchases.addAttributionData(data, from: AttributionNetwork.appleSearchAds) + Purchases.addAttributionData(data, fromNetwork: AttributionNetwork.appleSearchAds) for key in data.keys { expect(self.backend.invokedPostAttributionDataParametersList[0].data?.keys.contains(key)).toEventually(beTrue()) @@ -1822,23 +1841,23 @@ class PurchasesTests: XCTestCase { } func testSharedInstanceIsSetWhenConfiguring() { - let purchases = Purchases.configure(withAPIKey: "") + let purchases = Purchases.configure(apiKey: "") expect(Purchases.shared).toEventually(equal(purchases)) } func testSharedInstanceIsSetWhenConfiguringWithAppUserID() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "") + let purchases = Purchases.configure(apiKey: "", appUserID: "") expect(Purchases.shared).toEventually(equal(purchases)) } func testSharedInstanceIsSetWhenConfiguringWithObserverMode() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "", observerMode: true) + let purchases = Purchases.configure(apiKey: "", appUserID: "", observerMode: true) expect(Purchases.shared).toEventually(equal(purchases)) expect(Purchases.shared.finishTransactions).toEventually(beFalse()) } func testSharedInstanceIsSetWhenConfiguringWithAppUserIDAndUserDefaults() { - let purchases = Purchases.configure(withAPIKey: "", appUserID: "", observerMode: false, userDefaults: nil) + let purchases = Purchases.configure(apiKey: "", appUserID: "", observerMode: false, userDefaults: nil) expect(Purchases.shared).toEventually(equal(purchases)) expect(Purchases.shared.finishTransactions).toEventually(beTrue()) } @@ -2026,7 +2045,7 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "com.product.id1") guard let purchases = purchases else { fatalError("purchases is not initialized") } - purchases.purchaseProduct(product) { _,_,_,_ in } + purchases.purchase(product: product) { _,_,_,_ in } let transaction = MockTransaction() transaction.mockPayment = self.storeKitWrapper.payment! @@ -2051,7 +2070,7 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "com.product.id1") var receivedUserCancelled: Bool? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedUserCancelled = userCancelled } @@ -2070,7 +2089,7 @@ class PurchasesTests: XCTestCase { var receivedError: NSError? var receivedUnderlyingError: NSError? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error as NSError? receivedUserCancelled = userCancelled receivedUnderlyingError = receivedError?.userInfo[NSUnderlyingErrorKey] as! NSError? @@ -2098,7 +2117,7 @@ class PurchasesTests: XCTestCase { var receivedUserCancelled: Bool? var receivedError: NSError? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error as NSError? receivedUserCancelled = userCancelled } @@ -2127,7 +2146,13 @@ class PurchasesTests: XCTestCase { expect(self.storeKitWrapper.payment).to(beNil()) var completionCalled = false - self.purchasesDelegate.makeDeferredPurchase! { (tx, info, error, userCancelled) in + + guard let makeDeferredPurchase = purchasesDelegate.makeDeferredPurchase else { + fail("makeDeferredPurchase nil") + return + } + + makeDeferredPurchase { (tx, info, error, userCancelled) in completionCalled = true } @@ -2146,7 +2171,7 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "com.product.id1") let discount = SKPaymentDiscount.init(identifier: "discount", keyIdentifier: "TIKAMASALA1", nonce: UUID(), signature: "Base64 encoded signature", timestamp: NSNumber(value: Int64(123413232131))) - self.purchases?.purchaseProduct(product, discount: discount) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product, discount: discount) { (tx, info, error, userCancelled) in } @@ -2175,7 +2200,9 @@ class PurchasesTests: XCTestCase { var completionCalled = false var receivedPaymentDiscount: SKPaymentDiscount? - self.purchases?.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, error) in + self.purchases?.paymentDiscount(forProductDiscount: productDiscount, + product: product, + completion: { (paymentDiscount, error) in receivedPaymentDiscount = paymentDiscount completionCalled = true }) @@ -2207,7 +2234,9 @@ class PurchasesTests: XCTestCase { self.backend.postOfferForSigningPaymentDiscountResponse["timestamp"] = timestamp var maybeError: Error? - self.purchases?.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, error) in + self.purchases?.paymentDiscount(forProductDiscount: productDiscount, + product: product, + completion: { (paymentDiscount, error) in maybeError = error }) @@ -2229,7 +2258,9 @@ class PurchasesTests: XCTestCase { var completionCalled = false var receivedPaymentDiscount: SKPaymentDiscount? var receivedError: Error? = nil - self.purchases?.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, error) in + self.purchases?.paymentDiscount(forProductDiscount: productDiscount, + product: product, + completion: { (paymentDiscount, error) in receivedPaymentDiscount = paymentDiscount completionCalled = true receivedError = error @@ -2257,7 +2288,9 @@ class PurchasesTests: XCTestCase { var completionCalled = false var receivedPaymentDiscount: SKPaymentDiscount? var receivedError: Error? = nil - self.purchases?.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, error) in + self.purchases?.paymentDiscount(forProductDiscount: productDiscount, + product: product, + completion: { (paymentDiscount, error) in receivedPaymentDiscount = paymentDiscount completionCalled = true receivedError = error @@ -2274,7 +2307,7 @@ class PurchasesTests: XCTestCase { func testAttributionDataIsPostponedIfThereIsNoInstance() { let data = ["yo" : "dog", "what" : 45, "is" : ["up"]] as [String : Any] - Purchases.addAttributionData(data, from: AttributionNetwork.appsFlyer) + Purchases.addAttributionData(data, fromNetwork: AttributionNetwork.appsFlyer) setupPurchases() @@ -2294,7 +2327,7 @@ class PurchasesTests: XCTestCase { func testAttributionDataSendsNetworkAppUserId() { let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - Purchases.addAttributionData(data, from: AttributionNetwork.appleSearchAds, forNetworkUserId: "newuser") + Purchases.addAttributionData(data, fromNetwork: AttributionNetwork.appleSearchAds, forNetworkUserId: "newuser") setupPurchases() @@ -2313,7 +2346,7 @@ class PurchasesTests: XCTestCase { func testAttributionDataDontSendNetworkAppUserIdIfNotProvided() { let data = ["yo": "dog", "what": 45, "is": ["up"]] as [String: Any] - Purchases.addAttributionData(data, from: AttributionNetwork.appleSearchAds) + Purchases.addAttributionData(data, fromNetwork: AttributionNetwork.appleSearchAds) setupPurchases() @@ -2353,7 +2386,7 @@ class PurchasesTests: XCTestCase { func testObserverModeSetToFalseSetFinishTransactions() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2375,7 +2408,7 @@ class PurchasesTests: XCTestCase { func testDoesntFinishTransactionsIfObserverModeIsSet() { setupPurchasesObserverModeOn() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2397,7 +2430,7 @@ class PurchasesTests: XCTestCase { func testRestoredPurchasesArePosted() { setupPurchasesObserverModeOn() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2415,7 +2448,7 @@ class PurchasesTests: XCTestCase { setupPurchases() let product = SKProduct() var receivedError: Error? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error } @@ -2425,7 +2458,7 @@ class PurchasesTests: XCTestCase { func testNoCrashIfPaymentIsMissing() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } let transaction = SKPaymentTransaction() @@ -2455,7 +2488,7 @@ class PurchasesTests: XCTestCase { mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) self.purchases!.offerings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! - self.purchases!.purchasePackage(package) { (tx, info, error, userCancelled) in + self.purchases!.purchase(package: package) { (tx, info, error, userCancelled) in } @@ -2488,7 +2521,7 @@ class PurchasesTests: XCTestCase { let product = MockSKProduct(mockProductIdentifier: "Tacos_with_free_guac") let discount = MockDiscount() - purchases.paymentDiscount(for: discount, product: product) { discount, error in + purchases.paymentDiscount(forProductDiscount: discount, product: product) { discount, error in receivedError = error as NSError? receivedPaymentDiscount = discount } @@ -2505,8 +2538,8 @@ class PurchasesTests: XCTestCase { mockOfferingsManager.stubbedOfferingsCompletionResult = (offeringsFactory.createOfferings(withProducts: [:], data: [:]), nil) self.purchases!.offerings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! - self.purchases!.purchasePackage(package) { _,_,_,_ in - self.purchases!.purchasePackage(package) { (tx, info, error, userCancelled) in + self.purchases!.purchase(package: package) { _,_,_,_ in + self.purchases!.purchase(package: package) { (tx, info, error, userCancelled) in receivedError = error as NSError? secondCompletionCalled = true } @@ -2594,7 +2627,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsObserverModeWhenObserverMode() { setupPurchasesObserverModeOn() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2614,7 +2647,7 @@ class PurchasesTests: XCTestCase { func testReceiptsSendsObserverModeOffWhenObserverModeOff() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } @@ -2740,7 +2773,7 @@ class PurchasesTests: XCTestCase { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") var receivedError: NSError? - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in receivedError = error as NSError? } @@ -2761,7 +2794,7 @@ class PurchasesTests: XCTestCase { func testSyncsPurchasesIfEntitlementsRevokedForProductIDs() { if #available(iOS 14.0, macOS 14.0, tvOS 14.0, watchOS 7.0, *) { setupPurchases() - guard let purchases = purchases else { fatalError() } + guard let _ = purchases else { fatalError() } expect(self.backend.postReceiptDataCalled).to(beFalse()) (purchasesOrchestrator as StoreKitWrapperDelegate) .storeKitWrapper(storeKitWrapper, didRevokeEntitlementsForProductIdentifiers: ["a", "b"]) diff --git a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index 9b9f24cb07..9f2172e61d 100644 --- a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -10,10 +10,10 @@ // Created by RevenueCat on 3/01/20. // -import XCTest import Nimble +import StoreKit +import XCTest -@testable import Purchases @testable import PurchasesCoreSwift class PurchasesSubscriberAttributesTests: XCTestCase { @@ -104,7 +104,6 @@ class PurchasesSubscriberAttributesTests: XCTestCase { override func tearDown() { purchases?.delegate = nil purchases = nil - Purchases.setDefaultInstance(nil) UserDefaults().removePersistentDomain(forName: "TestDefaults") } @@ -147,11 +146,6 @@ class PurchasesSubscriberAttributesTests: XCTestCase { Purchases.setDefaultInstance(purchases!) } - func testInitializerConfiguresSubscriberAttributesManager() { - let purchases = Purchases.configure(withAPIKey: "key") - expect(purchases.subscriberAttributesManager).toNot(beNil()) - } - // Mark: Notifications func testSubscribesToForegroundNotifications() { @@ -279,7 +273,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { setupPurchases() let tokenString = "ligai32g32ig" - Purchases.shared._setPushTokenString(tokenString) + Purchases.shared.setPushTokenString(tokenString) expect(self.mockSubscriberAttributesManager.invokedSetPushTokenStringCount) == 1 let receivedPushToken = self.mockSubscriberAttributesManager.invokedSetPushTokenStringParameters!.pushToken! @@ -410,10 +404,11 @@ class PurchasesSubscriberAttributesTests: XCTestCase { // MARK: Post receipt with attributes + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptMarksSubscriberAttributesSyncedIfBackendSuccessful() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in } + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes let transaction = MockTransaction() @@ -435,10 +430,11 @@ class PurchasesSubscriberAttributesTests: XCTestCase { mockIdentityManager.currentAppUserID } + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptMarksSubscriberAttributesSyncedIfBackendSuccessfullySynced() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in } + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes let transaction = MockTransaction() @@ -463,10 +459,11 @@ class PurchasesSubscriberAttributesTests: XCTestCase { mockIdentityManager.currentAppUserID } + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *) func testPostReceiptDoesntMarkSubscriberAttributesSyncedIfBackendNotSuccessfullySynced() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") - self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in } + self.purchases?.purchase(product: product) { (tx, info, error, userCancelled) in } mockSubscriberAttributesManager.stubbedUnsyncedAttributesByKeyResult = mockAttributes let transaction = MockTransaction() diff --git a/RCPurchaserInfoAPI.m b/RCPurchaserInfoAPI.m index f45145c31f..a1ce0b7fbf 100644 --- a/RCPurchaserInfoAPI.m +++ b/RCPurchaserInfoAPI.m @@ -8,7 +8,6 @@ #import "RCPurchaserInfoAPI.h" -@import Purchases; @import PurchasesCoreSwift; @implementation RCPurchaserInfoAPI diff --git a/StoreKitTests/StoreKitTests.swift b/StoreKitTests/StoreKitTests.swift index b5cb795274..1959d3f942 100644 --- a/StoreKitTests/StoreKitTests.swift +++ b/StoreKitTests/StoreKitTests.swift @@ -22,7 +22,7 @@ class TestPurchaseDelegate: NSObject, PurchasesDelegate { func purchases(_ purchases: Purchases, shouldPurchasePromoProduct product: SKProduct, - defermentBlock makeDeferredPurchase: @escaping RCDeferredPromotionalPurchaseBlock) { + defermentBlock makeDeferredPurchase: @escaping DeferredPromotionalPurchaseBlock) { } }