diff --git a/Sources/Misc/StoreKitVersion.swift b/Sources/Misc/StoreKitVersion.swift index c985082768..f5bf8a6195 100644 --- a/Sources/Misc/StoreKitVersion.swift +++ b/Sources/Misc/StoreKitVersion.swift @@ -59,6 +59,21 @@ extension StoreKitVersion { } } + /// - Returns: `true` if SK2 is available and usable when PurchasesAreCompleted==.myApp on this device + // swiftlint:disable:next identifier_name + var isStoreKit2EnabledAndAvailableWhenPurchasesAreCompletedByMyApp: Bool { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + switch self { + case .storeKit1: + return false + case .storeKit2: + return true + } + } else { + return false + } + } + /// - Returns: `true` if and only if SK2 is enabled and it's available. var isStoreKit2EnabledAndAvailable: Bool { switch self { diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index b4cef05fbb..82fb8c1bd1 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -332,13 +332,10 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void diagnosticsTracker: diagnosticsTracker ) - let paymentQueueWrapper: EitherPaymentQueueWrapper = systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable - ? .right(.init()) - : .left(.init( - operationDispatcher: operationDispatcher, - observerMode: observerMode, - sandboxEnvironmentDetector: systemInfo - )) + let paymentQueueWrapper = Purchases.buildPaymentQueueWrapper( + systemInfo: systemInfo, + operationDispatcher: operationDispatcher + ) let offeringsFactory = OfferingsFactory() let receiptParser = PurchasesReceiptParser.default @@ -685,6 +682,30 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void } } + private static func buildPaymentQueueWrapper( + systemInfo: SystemInfo, + operationDispatcher: OperationDispatcher + ) -> EitherPaymentQueueWrapper { + if systemInfo.observerMode && + systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailableWhenPurchasesAreCompletedByMyApp { + // On iOS 15, we allow developers to make purchases with SK2 when PurchasesAreCompletedBy==.myApp. + // We need to specifically check for this case because otherwise we use SK1 on iOS 15, and if + // We are on iOS 15 with PurchasesAreCompletedBy==.myApp and SK2, we need to only listen to purchases + // via SK2. + return .right(PaymentQueueWrapper()) + } + + let paymentQueueWrapper: EitherPaymentQueueWrapper = systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable + ? .right(.init()) + : .left(.init( + operationDispatcher: operationDispatcher, + observerMode: systemInfo.observerMode, + sandboxEnvironmentDetector: systemInfo + )) + + return paymentQueueWrapper + } + /// - Parameter purchases: this is an `@autoclosure` to be able to clear the previous instance /// from memory before creating the new one. @discardableResult diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 478dde946a..739418f1fc 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -257,30 +257,48 @@ private extension TransactionPoster { func fetchEncodedReceipt(transaction: StoreTransactionType, completion: @escaping (Result) -> Void) { - if systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable, - let jwsRepresentation = transaction.jwsRepresentation { - if transaction.environment == .xcode, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { + + func handleTransactionWithJWSRepresentation(jwsRepresentation: String) { + if transaction.environment == .xcode, #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { _ = Task { completion(.success( - .sk2receipt(await self.transactionFetcher.fetchReceipt(containing: transaction)) - )) + .sk2receipt(await self.transactionFetcher.fetchReceipt(containing: transaction))) + ) } } else { completion(.success(.jws(jwsRepresentation))) } - } else { - self.receiptFetcher.receiptData( - refreshPolicy: self.refreshRequestPolicy(forProductIdentifier: transaction.productIdentifier) - ) { receiptData, receiptURL in - if let receiptData = receiptData, !receiptData.isEmpty { - completion(.success(.receipt(receiptData))) - } else { - completion(.failure(BackendError.missingReceiptFile(receiptURL))) - } + } + + if systemInfo.observerMode && + systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailableWhenPurchasesAreCompletedByMyApp, + let jwsRepresentation = transaction.jwsRepresentation { + // On iOS 15, we allow developers to make purchases with SK2 when PurchasesAreCompletedBy==.myApp. + // In this case, we should POST the SK2 receipt despite running on iOS 15, where we would normally + // POST a SK1 receipt if PurchasesAreCompletedBy==.revenueCat + + handleTransactionWithJWSRepresentation(jwsRepresentation: jwsRepresentation) + return + } + + if systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable, + let jwsRepresentation = transaction.jwsRepresentation { + + handleTransactionWithJWSRepresentation(jwsRepresentation: jwsRepresentation) + return + } + + // Fetch a SK1 receipt + self.receiptFetcher.receiptData( + refreshPolicy: self.refreshRequestPolicy(forProductIdentifier: transaction.productIdentifier) + ) { receiptData, receiptURL in + if let receiptData = receiptData, !receiptData.isEmpty { + completion(.success(.receipt(receiptData))) + } else { + completion(.failure(BackendError.missingReceiptFile(receiptURL))) } } } - } // MARK: - Properties