diff --git a/Networking/Networking/Model/WordPressApiError.swift b/Networking/Networking/Model/WordPressApiError.swift index df37f4220d5..2dccaba8f9d 100644 --- a/Networking/Networking/Model/WordPressApiError.swift +++ b/Networking/Networking/Model/WordPressApiError.swift @@ -8,6 +8,10 @@ public enum WordPressApiError: Error, Decodable, Equatable { /// case unknown(code: String, message: String) + /// An order already exists for this IAP receipt + /// + case productPurchased + /// Decodable Initializer. /// public init(from decoder: Decoder) throws { @@ -16,6 +20,8 @@ public enum WordPressApiError: Error, Decodable, Equatable { let message = try container.decode(String.self, forKey: .message) switch code { + case Constants.productPurchased: + self = .productPurchased default: self = .unknown(code: code, message: message) } @@ -25,6 +31,7 @@ public enum WordPressApiError: Error, Decodable, Equatable { /// Constants for Possible Error Identifiers /// private enum Constants { + static let productPurchased = "product_purchased" } /// Coding Keys @@ -50,6 +57,10 @@ extension WordPressApiError: CustomStringConvertible { public var description: String { switch self { + case .productPurchased: + return NSLocalizedString( + "An order aready exists for this receipt", + comment: "Error message when an order already exists in the backend for a given receipt") case .unknown(let code, let message): let messageFormat = NSLocalizedString( "WordPress API Error: [%1$@] %2$@", @@ -59,3 +70,11 @@ extension WordPressApiError: CustomStringConvertible { } } } + +// MARK: - LocalizedError Conformance +// +extension WordPressApiError: LocalizedError { + public var errorDescription: String? { + description + } +} diff --git a/Yosemite/Yosemite/Stores/InAppPurchaseStore.swift b/Yosemite/Yosemite/Stores/InAppPurchaseStore.swift index 7d7e7b5e3bd..c87028e087e 100644 --- a/Yosemite/Yosemite/Stores/InAppPurchaseStore.swift +++ b/Yosemite/Yosemite/Stores/InAppPurchaseStore.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Storage import StoreKit @@ -9,6 +10,7 @@ public class InAppPurchaseStore: Store { private var listenTask: Task? private let remote: InAppPurchasesRemote private var useBackend = true + private var pauseTransactionListener = CurrentValueSubject(false) public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) { remote = InAppPurchasesRemote(network: network) @@ -93,6 +95,12 @@ private extension InAppPurchaseStore { logInfo("Purchasing product \(product.id) for site \(siteID) with options \(purchaseOptions)") + logInfo("Pausing transaction listener") + pauseTransactionListener.send(true) + defer { + logInfo("Resuming transaction listener") + pauseTransactionListener.send(false) + } let purchaseResult = try await product.purchase(options: purchaseOptions) switch purchaseResult { case .success(let result): @@ -193,15 +201,22 @@ private extension InAppPurchaseStore { let receiptData = try await getAppReceipt() logInfo("Sending transaction to API for site \(siteID)") - let orderID = try await remote.createOrder( - for: siteID, - price: priceInCents, - productIdentifier: product.id, - appStoreCountryCode: countryCode, - receiptData: receiptData - ) - logInfo("Successfully registered purchase with Order ID \(orderID)") - + do { + let orderID = try await remote.createOrder( + for: siteID, + price: priceInCents, + productIdentifier: product.id, + appStoreCountryCode: countryCode, + receiptData: receiptData + ) + logInfo("Successfully registered purchase with Order ID \(orderID)") + } catch WordPressApiError.productPurchased { + // Ignore errors for existing purchase + logInfo("Existing order found for transaction \(transaction.id) on site \(siteID), ignoring") + } catch { + // Rethrow any other error + throw error + } } func userIsEntitledToProduct(with id: String) async throws -> Bool { @@ -251,11 +266,16 @@ private extension InAppPurchaseStore { assert(listenTask == nil, "InAppPurchaseStore.listenForTransactions() called while already listening for transactions") listenTask = Task.detached { [weak self] in + guard let self else { + return + } for await result in Transaction.updates { do { - try await self?.handleCompletedTransaction(result) + // Wait until the purchase finishes + _ = await self.pauseTransactionListener.values.contains(false) + try await self.handleCompletedTransaction(result) } catch { - self?.logError("Error handling transaction update: \(error)") + self.logError("Error handling transaction update: \(error)") } } }