summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2025-01-10 15:32:50 +0100
committerEmīls <emils@mullvad.net>2025-01-10 15:32:50 +0100
commitef57eee494bb2e618191c9448882843fdeb85b2b (patch)
treed48752d3d12748555f099face2c523fb96b8d736
parent550ad64c079c09ddad596528532b3913f105cf6f (diff)
parent6f21586e550737278fe233a6784d34092e000385 (diff)
downloadmullvadvpn-ef57eee494bb2e618191c9448882843fdeb85b2b.tar.xz
mullvadvpn-ef57eee494bb2e618191c9448882843fdeb85b2b.zip
Merge branch 'add-a-button-to-exercise-storekit2-in-debug-builds-ios-978'
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift3
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift12
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift12
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift62
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift24
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentState.swift3
6 files changed, 113 insertions, 3 deletions
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index a4ff7e3cd0..d7e5dd557b 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -521,7 +521,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let accountInteractor = AccountInteractor(
storePaymentManager: storePaymentManager,
tunnelManager: tunnelManager,
- accountsProxy: accountsProxy
+ accountsProxy: accountsProxy,
+ apiProxy: apiProxy
)
let coordinator = AccountCoordinator(
diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
index bb5a3ccf8e..bedcdbdf04 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
@@ -15,6 +15,17 @@ class AccountContentView: UIView {
return button
}()
+ let storeKit2Button: AppButton = {
+ let button = AppButton(style: .success)
+ button.setTitle(NSLocalizedString(
+ "BUY_SUBSCRIPTION_STOREKIT_2",
+ tableName: "Account",
+ value: "Make a purchase with StoreKit2",
+ comment: ""
+ ), for: .normal)
+ return button
+ }()
+
let redeemVoucherButton: AppButton = {
let button = AppButton(style: .success)
button.setAccessibilityIdentifier(.redeemVoucherButton)
@@ -85,6 +96,7 @@ class AccountContentView: UIView {
var arrangedSubviews = [UIView]()
#if DEBUG
arrangedSubviews.append(redeemVoucherButton)
+ arrangedSubviews.append(storeKit2Button)
#endif
arrangedSubviews.append(contentsOf: [
purchaseButton,
diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
index f5cc3a77a5..b282c48cac 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
@@ -17,6 +17,7 @@ final class AccountInteractor {
private let storePaymentManager: StorePaymentManager
let tunnelManager: TunnelManager
let accountsProxy: RESTAccountHandling
+ let apiProxy: APIQuerying
var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)?
var didReceiveDeviceState: ((DeviceState) -> Void)?
@@ -27,11 +28,13 @@ final class AccountInteractor {
init(
storePaymentManager: StorePaymentManager,
tunnelManager: TunnelManager,
- accountsProxy: RESTAccountHandling
+ accountsProxy: RESTAccountHandling,
+ apiProxy: APIQuerying
) {
self.storePaymentManager = storePaymentManager
self.tunnelManager = tunnelManager
self.accountsProxy = accountsProxy
+ self.apiProxy = apiProxy
let tunnelObserver =
TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, _ in
@@ -61,6 +64,13 @@ final class AccountInteractor {
storePaymentManager.addPayment(payment, for: accountNumber)
}
+ func sendStoreKitReceipt(_ transaction: VerificationResult<Transaction>, for accountNumber: String) async throws {
+ try await apiProxy.createApplePayment(
+ accountNumber: accountNumber,
+ receiptString: transaction.jwsRepresentation.data(using: .utf8)!
+ ).execute()
+ }
+
func restorePurchases(
for accountNumber: String,
completionHandler: @escaping (Result<REST.CreateApplePaymentResponse, Error>) -> Void
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index f39a228e66..f814378c2f 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -144,6 +144,8 @@ class AccountViewController: UIViewController {
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)
}
private func requestStoreProducts() {
@@ -202,6 +204,7 @@ class AccountViewController: UIViewController {
contentView.logoutButton.isEnabled = isInteractionEnabled
contentView.redeemVoucherButton.isEnabled = isInteractionEnabled
contentView.deleteButton.isEnabled = isInteractionEnabled
+ contentView.storeKit2Button.isEnabled = isInteractionEnabled
navigationItem.rightBarButtonItem?.isEnabled = isInteractionEnabled
view.isUserInteractionEnabled = isInteractionEnabled
@@ -293,4 +296,63 @@ class AccountViewController: UIViewController {
setPaymentState(.none, animated: true)
}
}
+
+ @objc private func handleStoreKit2Purchase() {
+ guard case let .received(oldProduct) = productState,
+ let accountData = interactor.deviceState.accountData
+ else {
+ return
+ }
+
+ setPaymentState(.makingStoreKit2Purchase, animated: true)
+
+ Task {
+ do {
+ let product = try await Product.products(for: [oldProduct.productIdentifier]).first!
+ let result = try await product.purchase()
+
+ switch result {
+ case let .success(verification):
+ let transaction = try checkVerified(verification)
+ await sendReceiptToAPI(accountNumber: accountData.identifier, receipt: verification)
+ await transaction.finish()
+
+ case .userCancelled:
+ print("User cancelled the purchase")
+ case .pending:
+ print("Purchase is pending")
+ @unknown default:
+ print("Unknown purchase result")
+ }
+ } 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:
+ throw StoreKit2Error.verificationFailed
+ case let .verified(safe):
+ return safe
+ }
+ }
+
+ private func sendReceiptToAPI(accountNumber: String, receipt: VerificationResult<Transaction>) async {
+ do {
+ try await interactor.sendStoreKitReceipt(receipt, for: accountNumber)
+ print("Receipt sent successfully")
+ } catch {
+ print("Error sending receipt: \(error)")
+ errorPresenter.showAlertForStoreKitError(error, context: .purchase)
+ }
+ }
+}
+
+private enum StoreKit2Error: Error {
+ case verificationFailed
}
diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
index 0192f3fdd3..82820ba3fb 100644
--- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
+++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
@@ -36,6 +36,30 @@ struct PaymentAlertPresenter {
presenter.showAlert(presentation: presentation, animated: true)
}
+ func showAlertForStoreKitError(
+ _ error: any Error,
+ context: REST.CreateApplePaymentResponse.Context,
+ completion: (() -> Void)? = nil
+ ) {
+ let presentation = AlertPresentation(
+ id: "payment-error-alert",
+ title: context.errorTitle,
+ message: "\(error)",
+ buttons: [
+ AlertAction(
+ title: okButtonTextForKey("PAYMENT_ERROR_ALERT_OK_ACTION"),
+ style: .default,
+ handler: {
+ completion?()
+ }
+ ),
+ ]
+ )
+
+ let presenter = AlertPresenter(context: alertContext)
+ presenter.showAlert(presentation: presentation, animated: true)
+ }
+
func showAlertForResponse(
_ response: REST.CreateApplePaymentResponse,
context: REST.CreateApplePaymentResponse.Context,
diff --git a/ios/MullvadVPN/View controllers/Account/PaymentState.swift b/ios/MullvadVPN/View controllers/Account/PaymentState.swift
index b50263f831..fa397d94d5 100644
--- a/ios/MullvadVPN/View controllers/Account/PaymentState.swift
+++ b/ios/MullvadVPN/View controllers/Account/PaymentState.swift
@@ -12,13 +12,14 @@ import StoreKit
enum PaymentState: Equatable {
case none
case makingPayment(SKPayment)
+ case makingStoreKit2Purchase
case restoringPurchases
var allowsViewInteraction: Bool {
switch self {
case .none:
return true
- case .restoringPurchases, .makingPayment:
+ case .restoringPurchases, .makingPayment, .makingStoreKit2Purchase:
return false
}
}