diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-09 13:48:43 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-09 13:48:43 +0200 |
| commit | 6b115f7af908eeaff8ea9f1afcc281480447b57e (patch) | |
| tree | 77e374e16ce37edbfe6c781f8c10879a554a5943 | |
| parent | 56f4ce0acb85bf38809b17a9a29b1eaea7a5ed0f (diff) | |
| parent | 4bfc63c055803474068af3f33046688834f317ef (diff) | |
| download | mullvadvpn-6b115f7af908eeaff8ea9f1afcc281480447b57e.tar.xz mullvadvpn-6b115f7af908eeaff8ea9f1afcc281480447b57e.zip | |
Merge branch 'create-button-for-storekit-2-refunds-ios-1153'
5 files changed, 94 insertions, 11 deletions
diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift index cf6ad054ad..1513108013 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift @@ -21,7 +21,7 @@ class AccountContentView: UIView { return button }() - let storeKit2Button: AppButton = { + let storeKit2PurchaseButton: AppButton = { let button = AppButton(style: .success) button.setTitle(NSLocalizedString( "BUY_SUBSCRIPTION_STOREKIT_2", @@ -32,6 +32,17 @@ class AccountContentView: UIView { return button }() + let storeKit2RefundButton: AppButton = { + let button = AppButton(style: .success) + button.setTitle(NSLocalizedString( + "BUY_SUBSCRIPTION_STOREKIT_2", + tableName: "Account", + value: "Refund last purchase with StoreKit2", + comment: "" + ), for: .normal) + return button + }() + let redeemVoucherButton: AppButton = { let button = AppButton(style: .success) button.setAccessibilityIdentifier(.redeemVoucherButton) @@ -102,7 +113,8 @@ class AccountContentView: UIView { var arrangedSubviews = [UIView]() #if DEBUG arrangedSubviews.append(redeemVoucherButton) - arrangedSubviews.append(storeKit2Button) + arrangedSubviews.append(storeKit2PurchaseButton) + arrangedSubviews.append(storeKit2RefundButton) #endif arrangedSubviews.append(contentsOf: [ purchaseButton, diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index 1a6583238e..8a6f44df71 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -50,7 +50,7 @@ final class AccountInteractor: Sendable { } func sendStoreKitReceipt(_ transaction: VerificationResult<Transaction>, for accountNumber: String) async throws { - try await apiProxy.createApplePayment( + _ = try await apiProxy.createApplePayment( accountNumber: accountNumber, receiptString: transaction.jwsRepresentation.data(using: .utf8)! ).execute() diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 7ad04a083c..6f362e96db 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -40,6 +40,7 @@ class AccountViewController: UIViewController, @unchecked Sendable { private var isFetchingProducts = false private var paymentState: PaymentState = .none + private let storeKit2TestProduct = StoreSubscription.thirtyDays.rawValue var actionHandler: ActionHandler? @@ -135,10 +136,15 @@ class AccountViewController: UIViewController, @unchecked Sendable { ) contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside) - contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside) - - contentView.storeKit2Button.addTarget(self, action: #selector(handleStoreKit2Purchase), for: .touchUpInside) + contentView.storeKit2PurchaseButton.addTarget( + self, action: #selector(handleStoreKit2Purchase), + for: .touchUpInside + ) + contentView.storeKit2RefundButton.addTarget( + self, action: #selector(handleStoreKit2Refund), + for: .touchUpInside + ) } @MainActor @@ -175,7 +181,8 @@ class AccountViewController: UIViewController, @unchecked Sendable { contentView.logoutButton.isEnabled = isInteractionEnabled contentView.redeemVoucherButton.isEnabled = isInteractionEnabled contentView.deleteButton.isEnabled = isInteractionEnabled - contentView.storeKit2Button.isEnabled = isInteractionEnabled + contentView.storeKit2PurchaseButton.isEnabled = isInteractionEnabled + contentView.storeKit2RefundButton.isEnabled = isInteractionEnabled navigationItem.rightBarButtonItem?.isEnabled = isInteractionEnabled view.isUserInteractionEnabled = isInteractionEnabled @@ -223,13 +230,11 @@ class AccountViewController: UIViewController, @unchecked Sendable { return } - let productIdentifiers = StoreSubscription.allCases.map { $0.rawValue } - setPaymentState(.makingStoreKit2Purchase, animated: true) Task { do { - let product = try await Product.products(for: productIdentifiers).first! + let product = try await Product.products(for: [storeKit2TestProduct]).first! let result = try await product.purchase() switch result { @@ -253,6 +258,41 @@ class AccountViewController: UIViewController, @unchecked Sendable { } } + @objc private func handleStoreKit2Refund() { + setPaymentState(.makingStoreKit2Refund, animated: true) + + Task { + guard + let latestTransactionResult = await Transaction.latest(for: storeKit2TestProduct), + let windowScene = view.window?.windowScene + else { return } + + do { + switch latestTransactionResult { + case let .verified(transaction): + let refundStatus = try await transaction.beginRefundRequest(in: windowScene) + + switch refundStatus { + case .success: + print("Refund was successful") + errorPresenter.showAlertForRefund() + case .userCancelled: + print("User cancelled the refund") + @unknown default: + print("Unknown refund result") + } + case .unverified: + print("Transaction is unverified") + } + } catch { + print("Error: \(error)") + errorPresenter.showAlertForStoreKitError(error, context: .purchase) + } + + setPaymentState(.none, animated: true) + } + } + private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T { switch result { case .unverified: diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift index 276e94a221..f78ef062e0 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift @@ -13,6 +13,36 @@ import Routing struct PaymentAlertPresenter { let alertContext: any Presenting + func showAlertForRefund(completion: (@MainActor @Sendable () -> Void)? = nil) { + let presentation = AlertPresentation( + id: "payment-refund-alert", + title: NSLocalizedString( + "PAYMENT_REFUND_ALERT_TITLE", + tableName: "Payment", + value: "Refund successful", + comment: "" + ), + message: NSLocalizedString( + "PAYMENT_REFUND_ALERT_MESSAGE", + tableName: "Payment", + value: "Your purchase was successfully refunded.", + comment: "" + ), + buttons: [ + AlertAction( + title: okButtonTextForKey("PAYMENT_REFUND_ALERT_OK_ACTION"), + style: .default, + handler: { + completion?() + } + ), + ] + ) + + let presenter = AlertPresenter(context: alertContext) + presenter.showAlert(presentation: presentation, animated: true) + } + func showAlertForError( _ error: StorePaymentManagerError, context: REST.CreateApplePaymentResponse.Context, diff --git a/ios/MullvadVPN/View controllers/Account/PaymentState.swift b/ios/MullvadVPN/View controllers/Account/PaymentState.swift index 80625cc537..93322e7e62 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentState.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentState.swift @@ -13,13 +13,14 @@ enum PaymentState: Equatable { case none case makingPayment(SKPayment) case makingStoreKit2Purchase + case makingStoreKit2Refund case restoringPurchases var allowsViewInteraction: Bool { switch self { case .none: return true - case .restoringPurchases, .makingPayment, .makingStoreKit2Purchase: + case .restoringPurchases, .makingPayment, .makingStoreKit2Purchase, .makingStoreKit2Refund: return false } } |
