summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-04-09 13:48:43 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-04-09 13:48:43 +0200
commit6b115f7af908eeaff8ea9f1afcc281480447b57e (patch)
tree77e374e16ce37edbfe6c781f8c10879a554a5943
parent56f4ce0acb85bf38809b17a9a29b1eaea7a5ed0f (diff)
parent4bfc63c055803474068af3f33046688834f317ef (diff)
downloadmullvadvpn-6b115f7af908eeaff8ea9f1afcc281480447b57e.tar.xz
mullvadvpn-6b115f7af908eeaff8ea9f1afcc281480447b57e.zip
Merge branch 'create-button-for-storekit-2-refunds-ios-1153'
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift16
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift54
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift30
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentState.swift3
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
}
}