mirror of
https://github.com/ProtonMail/protoncore_ios.git
synced 2026-01-16 23:00:24 +00:00
Fix: [P2-511] UI is blocked when a renewal transaction is received
Refs: https://gitlab.protontech.ch/apple/shared/protoncore/-/merge_requests/2123
This commit is contained in:
commit
08d5f57341
10 changed files with 63 additions and 21 deletions
|
|
@ -115,7 +115,7 @@ class PaymentsManager {
|
|||
case .purchasedPlan(let plan):
|
||||
self?.selectedPlan = plan
|
||||
completionHandler(.success(()))
|
||||
case .purchaseError(let error):
|
||||
case .purchaseError(let error), .planAlreadyPurchased(let error):
|
||||
if !shownHandlerCalled {
|
||||
planShownHandler?()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public enum PurchaseResult {
|
|||
case purchaseError(error: Error, processingPlan: InAppPurchasePlan? = nil)
|
||||
case apiMightBeBlocked(message: String, originalError: Error, processingPlan: InAppPurchasePlan? = nil)
|
||||
case purchaseCancelled
|
||||
case planAlreadyPurchased(error: Error)
|
||||
@available(*, deprecated, message: "Renewal transactions are no longer reported on")
|
||||
case renewalNotification
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ public enum PurchaseResult {
|
|||
case .apiMightBeBlocked: .apiBlocked
|
||||
case .purchaseCancelled: .canceled
|
||||
case .renewalNotification: .renewalNotification
|
||||
case .planAlreadyPurchased: .canceled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ public enum PurchaseResult {
|
|||
case .apiMightBeBlocked: .apiBlocked
|
||||
case .purchaseCancelled: .canceled
|
||||
case .renewalNotification: .unknown
|
||||
case .planAlreadyPurchased: .canceled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +69,7 @@ public enum PurchaseResult {
|
|||
case .apiMightBeBlocked: .apiMightBeBlocked
|
||||
case .purchaseCancelled: .canceled
|
||||
case .renewalNotification: .successful
|
||||
case .planAlreadyPurchased: .canceled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,6 +292,8 @@ final class PurchaseManager: PurchaseManagerProtocol {
|
|||
finishCallback(.purchaseCancelled)
|
||||
} else if let error = error as? StoreKitManagerErrors, case let .apiMightBeBlocked(message, originalError) = error {
|
||||
finishCallback(.apiMightBeBlocked(message: message, originalError: originalError, processingPlan: self?.unfinishedPurchasePlan))
|
||||
} else if (error as? StoreKitManagerErrors) == .renewalTransaction {
|
||||
finishCallback(.planAlreadyPurchased(error: error))
|
||||
} else {
|
||||
finishCallback(.purchaseError(error: error, processingPlan: self?.unfinishedPurchasePlan))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,10 @@ final class StoreKitManager: NSObject, StoreKitManagerProtocol {
|
|||
threadSafeCache.removeValue(for: cache, in: \.errorCompletion, defaultValue: defaultErrorCallback) { $0?(error) }
|
||||
}
|
||||
|
||||
private func callSilentErrorCompletion(for cache: UserInitiatedPurchaseCache, with error: Error) {
|
||||
threadSafeCache.removeValue(for: cache, in: \.errorCompletion, defaultValue: {_ in }) { $0?(error) }
|
||||
}
|
||||
|
||||
private func getSuccessCompletion(for cache: UserInitiatedPurchaseCache, completion: @escaping (SuccessCallback?) -> Void) {
|
||||
threadSafeCache.removeValue(for: cache, in: \.successCompletion, completion: completion)
|
||||
}
|
||||
|
|
@ -661,8 +665,11 @@ extension StoreKitManager: SKPaymentTransactionObserver {
|
|||
guard !transaction.isRenewal else {
|
||||
// skip processing for transactions corresponding to a monthly renewal
|
||||
if let self {
|
||||
paymentQueue.finishTransaction(transaction)
|
||||
finishTransaction(transaction, completion: nil)
|
||||
ObservabilityEnv.report(.paymentLaunchBillingTotal(status: .renewalNotification, isDynamic: self.featureFlagsRepository.isEnabled(CoreFeatureFlagType.dynamicPlan)))
|
||||
let cacheKey = UserInitiatedPurchaseCache(storeKitProductId: transaction.payment.productIdentifier,
|
||||
hashedUserId: applicationUserId().map(hash(userId:)))
|
||||
self.callSilentErrorCompletion(for: cacheKey, with: StoreKitManagerErrors.renewalTransaction)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,9 +180,7 @@ protocol ProcessDependencies: AnyObject {
|
|||
var refreshCompletionHandler: (ProcessCompletionResult) -> Void { get }
|
||||
}
|
||||
|
||||
extension SKPaymentQueue: PaymentQueueProtocol {
|
||||
|
||||
}
|
||||
extension SKPaymentQueue: PaymentQueueProtocol {}
|
||||
|
||||
extension InAppPurchasePlan {
|
||||
// only needed for backwards compatibility to purchase 0 cost plans
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public enum StoreKitManagerErrors: LocalizedError {
|
|||
case appIsLocked
|
||||
case pleaseSignIn
|
||||
case apiMightBeBlocked(message: String, originalError: Error)
|
||||
case renewalTransaction
|
||||
|
||||
@available(*, deprecated, message: "This is never returned anymore — the success callback with `.resolvingIAPToCreditsCausedByError` is returned instead")
|
||||
static var creditsApplied: StoreKitManagerErrors { .transactionFailedByUnknownReason }
|
||||
|
|
@ -52,19 +53,34 @@ public enum StoreKitManagerErrors: LocalizedError {
|
|||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .unavailableProduct: return PSTranslation._error_unavailable_product.l10n
|
||||
case .invalidPurchase: return PSTranslation._error_invalid_purchase.l10n
|
||||
case .receiptLost: return PSTranslation._error_receipt_lost.l10n
|
||||
case .haveTransactionOfAnotherUser: return PSTranslation._error_another_user_transaction.l10n
|
||||
case .alreadyPurchasedPlanDoesNotMatchBackend: return PSTranslation._error_backend_mismatch.l10n
|
||||
case .noActiveUsername: return PSTranslation._error_no_active_username_in_user_data_service.l10n
|
||||
case .transactionFailedByUnknownReason: return PSTranslation._error_transaction_failed_by_unknown_reason.l10n
|
||||
case .noNewSubscriptionInSuccessfulResponse: return PSTranslation._error_no_new_subscription_in_response.l10n
|
||||
case .appIsLocked: return PSTranslation._error_unlock_to_proceed_with_iap.l10n
|
||||
case .pleaseSignIn: return PSTranslation._error_please_sign_in_iap.l10n
|
||||
case .wrongTokenStatus: return PSTranslation._error_wrong_token_status.l10n
|
||||
case .notAllowed, .unknown: return nil
|
||||
case .apiMightBeBlocked(let message, _): return message
|
||||
case .unavailableProduct:
|
||||
return PSTranslation._error_unavailable_product.l10n
|
||||
case .invalidPurchase:
|
||||
return PSTranslation._error_invalid_purchase.l10n
|
||||
case .receiptLost:
|
||||
return PSTranslation._error_receipt_lost.l10n
|
||||
case .haveTransactionOfAnotherUser:
|
||||
return PSTranslation._error_another_user_transaction.l10n
|
||||
case .alreadyPurchasedPlanDoesNotMatchBackend:
|
||||
return PSTranslation._error_backend_mismatch.l10n
|
||||
case .noActiveUsername:
|
||||
return PSTranslation._error_no_active_username_in_user_data_service.l10n
|
||||
case .transactionFailedByUnknownReason:
|
||||
return PSTranslation._error_transaction_failed_by_unknown_reason.l10n
|
||||
case .noNewSubscriptionInSuccessfulResponse:
|
||||
return PSTranslation._error_no_new_subscription_in_response.l10n
|
||||
case .appIsLocked:
|
||||
return PSTranslation._error_unlock_to_proceed_with_iap.l10n
|
||||
case .pleaseSignIn:
|
||||
return PSTranslation._error_please_sign_in_iap.l10n
|
||||
case .wrongTokenStatus:
|
||||
return PSTranslation._error_wrong_token_status.l10n
|
||||
case .notAllowed, .unknown:
|
||||
return nil
|
||||
case .apiMightBeBlocked(let message, _):
|
||||
return message
|
||||
case .renewalTransaction:
|
||||
return PSTranslation.error_plan_already_purchased.l10n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +98,8 @@ extension StoreKitManagerErrors: Equatable {
|
|||
(.noNewSubscriptionInSuccessfulResponse, .noNewSubscriptionInSuccessfulResponse),
|
||||
(.notAllowed, .notAllowed),
|
||||
(.appIsLocked, .appIsLocked),
|
||||
(.pleaseSignIn, .pleaseSignIn):
|
||||
(.pleaseSignIn, .pleaseSignIn),
|
||||
(.renewalTransaction, .renewalTransaction):
|
||||
return true
|
||||
case let (.wrongTokenStatus(ltoken), .wrongTokenStatus(rtoken)):
|
||||
return ltoken == rtoken
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public enum PSTranslation: TranslationsExposing {
|
|||
case popup_credits_applied_cancellation
|
||||
case error_apply_payment_on_registration_title
|
||||
case error_apply_payment_on_registration_message
|
||||
case error_plan_already_purchased
|
||||
|
||||
public var l10n: String {
|
||||
switch self {
|
||||
|
|
@ -104,6 +105,8 @@ public enum PSTranslation: TranslationsExposing {
|
|||
return localized(key: "Payment failed", comment: "Error applying credit after registration alert")
|
||||
case .error_apply_payment_on_registration_message:
|
||||
return localized(key: "You have successfully registered but your payment was not processed. To resend your payment information, click Retry. You will only be charged once. If the problem persists, please contact customer support.", comment: "Error applying credit after registration alert")
|
||||
case .error_plan_already_purchased:
|
||||
return localized(key: "The signed-in Apple account already purchased this item.", comment: "Error trying to purchase a plan twice using the same Apple account")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,3 +43,5 @@
|
|||
"OK" = "OK";
|
||||
|
||||
"The Proton servers are unreachable. It might be caused by wrong network configuration, Proton servers not working or Proton servers being blocked" = "The Proton servers are unreachable. It might be caused by wrong network configuration, Proton servers not working or Proton servers being blocked";
|
||||
|
||||
"The signed-in Apple account already purchased this item." = "The signed-in Apple account already purchased this item.";
|
||||
|
|
|
|||
|
|
@ -397,6 +397,10 @@ extension PaymentsUICoordinator: PaymentsUIViewControllerDelegate {
|
|||
break
|
||||
case .renewalNotification:
|
||||
break // precondition prevents it
|
||||
case .planAlreadyPurchased(let error):
|
||||
self.unfinishedPurchasePlan = nil
|
||||
self.finishCallback(reason: .planAlreadyPurchased(error: error))
|
||||
self.showError(error: error)
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public enum PaymentsUIResultReason {
|
|||
case planPurchaseProcessingInProgress(accountPlan: InAppPurchasePlan)
|
||||
case purchaseError(error: Error)
|
||||
case apiMightBeBlocked(message: String, originalError: Error)
|
||||
case planAlreadyPurchased(error: Error)
|
||||
}
|
||||
|
||||
enum PaymentsUIMode {
|
||||
|
|
|
|||
|
|
@ -244,7 +244,9 @@ public class ProtonButton: UIButton, AccessibleView {
|
|||
fileprivate func showLoading() {
|
||||
contentEdgeInsets = UIEdgeInsets(top: contentEdgeInsets.top, left: 40, bottom: contentEdgeInsets.bottom, right: 40)
|
||||
if let activityIndicator = activityIndicator {
|
||||
activityIndicator.startAnimating()
|
||||
DispatchQueue.main.async {
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
} else {
|
||||
createActivityIndicator()
|
||||
}
|
||||
|
|
@ -253,7 +255,9 @@ public class ProtonButton: UIButton, AccessibleView {
|
|||
|
||||
fileprivate func stopLoading() {
|
||||
modeConfiguration()
|
||||
activityIndicator?.stopAnimating()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.activityIndicator?.stopAnimating()
|
||||
}
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue