fix: Correct flow completion for purchases initiated during signup [MAILIOS-4363]

Refs: https://gitlab.protontech.ch/apple/shared/protoncore/-/merge_requests/1852
This commit is contained in:
Victor Jalencas 2024-06-28 17:32:43 +00:00
commit ccb654dd5d
5 changed files with 28 additions and 29 deletions

View file

@ -29,6 +29,7 @@ import ProtonCorePayments
import ProtonCorePaymentsUI
import ProtonCoreHumanVerification
import ProtonCoreFoundations
import ProtonCoreLog
enum FlowStartKind {
case over(UIViewController, UIModalTransitionStyle)
@ -575,6 +576,7 @@ extension SignupCoordinator: CompleteViewControllerDelegate {
// swiftlint:disable:next cyclomatic_complexity
private func errorHandler(error: Error) {
PMLog.error(error, sendToExternal: true)
longTermTask.inProgress = false
if activeViewController != nil {
navigationController?.popViewController(animated: true)

View file

@ -126,8 +126,9 @@ class PaymentsManager {
break
case .toppedUpCredits:
completionHandler(.success(()))
case .planPurchaseProcessingInProgress:
break
case .planPurchaseProcessingInProgress(let plan):
self?.selectedPlan = plan
completionHandler(.success(()))
}
})
}
@ -154,11 +155,13 @@ class PaymentsManager {
}
case .right(let planDataSource):
Task { [weak self] in
do {
try await planDataSource.fetchCurrentPlan()
self?.payments.storeKitManager.retryProcessingAllPendingTransactions { [weak self] in
payments.storeKitManager.retryProcessingAllPendingTransactions { [weak self] in
Task { [weak self] in
do {
var possiblyPurchasedPlan: InAppPurchasePlan?
try await planDataSource.fetchCurrentPlan()
if planDataSource.currentPlan?.hasExistingProtonSubscription ?? false {
possiblyPurchasedPlan = self?.selectedPlan
}
@ -166,9 +169,9 @@ class PaymentsManager {
self?.restoreExistingDelegate()
self?.payments.storeKitManager.unsubscribeFromPaymentQueue()
completionHandler(.success(possiblyPurchasedPlan))
} catch {
completionHandler(.failure(error))
}
} catch {
completionHandler(.failure(error))
}
}
}

View file

@ -245,6 +245,10 @@ final class PurchaseManager: PurchaseManagerProtocol {
finishCallback(.toppedUpCredits)
} else if case .autoRenewal = result {
finishCallback(.renewalNotification)
} else if case .withoutObtainingToken = result { // purchase during signup flow
finishCallback(.planPurchaseProcessingInProgress(processingPlan: plan))
} else if case .withoutExchangingToken = result { // purchase during signup flow
finishCallback(.planPurchaseProcessingInProgress(processingPlan: plan))
} else {
finishCallback(.purchasedPlan(accountPlan: plan))
}

View file

@ -529,6 +529,7 @@ final class StoreKitManager: NSObject, StoreKitManagerProtocol {
errorCompletion: @escaping ErrorCallback,
deferredCompletion: (() -> Void)? = nil) {
subscribeToPaymentQueue()
let callbackCacheKey = UserInitiatedPurchaseCache(storeKitProductId: storeKitProduct.productIdentifier,
hashedUserId: hashedUserId)
threadSafeCache.set(value: successCompletion, for: callbackCacheKey, in: \.successCompletion)
@ -685,7 +686,7 @@ extension StoreKitManager: SKPaymentTransactionObserver {
// reappearing on every run
callSuccessCompletion(for: cacheKey, with: .withPurchaseAlreadyProcessed)
@unknown default:
break
PMLog.error("Unexpected unknown transaction state: \(transaction.transactionState)", sendToExternal: true)
}
}
@ -744,6 +745,7 @@ extension StoreKitManager: SKPaymentTransactionObserver {
try informProtonBackendAboutPurchasedTransaction(transaction, cacheKey: cacheKey, completion: completion)
} catch Errors.haveTransactionOfAnotherUser { // user login error
PMLog.error("Error: transaction is from another account", sendToExternal: true)
confirmUserValidationBypass(cacheKey, Errors.haveTransactionOfAnotherUser) { [weak self] in
self?.transactionsQueue.addOperation { [weak self] in
self?.processStoreKitTransaction(transaction: transaction, shouldVerifyPurchaseWasForSameAccount: false)
@ -752,16 +754,19 @@ extension StoreKitManager: SKPaymentTransactionObserver {
}
} catch Errors.receiptLost { // receipt error
PMLog.error("Error: lost receipt", sendToExternal: true)
callErrorCompletion(for: cacheKey, with: Errors.receiptLost)
finishTransaction(transaction, nil)
group.leave()
} catch Errors.noNewSubscriptionInSuccessfulResponse { // error on BE
PMLog.error("Error: no new subscription in success response", sendToExternal: true)
callErrorCompletion(for: cacheKey, with: Errors.noNewSubscriptionInSuccessfulResponse)
finishTransaction(transaction, nil)
group.leave()
} catch Errors.alreadyPurchasedPlanDoesNotMatchBackend {
PMLog.error("Error: Already purchased plan does not match backend", sendToExternal: true)
callErrorCompletion(for: cacheKey, with: Errors.alreadyPurchasedPlanDoesNotMatchBackend)
if featureFlagsRepository.isEnabled(CoreFeatureFlagType.dynamicPlan) &&
@ -773,6 +778,7 @@ extension StoreKitManager: SKPaymentTransactionObserver {
group.leave()
} catch let error { // other errors
PMLog.error("Error: \(error)", sendToExternal: true)
callErrorCompletion(for: cacheKey, with: error)
// should we call finishTransaction here, to avoid leaving transactions for the next run?
group.leave()
@ -862,8 +868,10 @@ extension StoreKitManager: SKPaymentTransactionObserver {
do {
let customCompletion: ProcessCompletionCallback = { result in
switch result {
case .finished:
ObservabilityEnv.report(.paymentSubscribeTotal(status: .successful, isDynamic: isDynamic))
case let .finished(result):
if case .withoutExchangingToken = result { } else {
ObservabilityEnv.report(.paymentSubscribeTotal(status: .successful, isDynamic: isDynamic))
}
case .errored, .erroredWithUnspecifiedError:
ObservabilityEnv.report(.paymentSubscribeTotal(status: .failed, isDynamic: isDynamic))
}

View file

@ -103,24 +103,6 @@ public extension StoreKitManagerProtocol {
retryProcessingAllPendingTransactions(finishHandler: finishHandler)
}
@available(*, deprecated, message: "Please use SuccessCallback")
typealias OldDeprecatedSuccessCallback = (PaymentToken?) -> Void
@available(*, deprecated, message: "Switch to variant using the SuccessCallback")
func purchaseProduct(plan: InAppPurchasePlan,
amountDue: Int,
successCompletion: @escaping OldDeprecatedSuccessCallback,
errorCompletion: @escaping ErrorCallback,
deferredCompletion: FinishCallback?) {
purchaseProduct(plan: plan, amountDue: amountDue, successCompletion: { result in
switch result {
case .withoutExchangingToken(let token):
successCompletion(token)
case .cancelled, .withoutIAP, .withoutObtainingToken, .withPurchaseAlreadyProcessed, .resolvingIAPToSubscription, .resolvingIAPToCredits, .resolvingIAPToCreditsCausedByError, .autoRenewal:
successCompletion(nil)
}
}, errorCompletion: errorCompletion, deferredCompletion: deferredCompletion)
}
}
protocol PaymentQueueProtocol {