Skip to content

Commit

Permalink
Add StoreKit2 button
Browse files Browse the repository at this point in the history
  • Loading branch information
pinkisemils committed Dec 27, 2024
1 parent de998f8 commit 911692b
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 3 deletions.
1 change: 1 addition & 0 deletions ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public enum AccessibilityIdentifier: Equatable {
case loginTextFieldButton
case logoutButton
case purchaseButton
case storeKit2Button
case redeemVoucherButton
case restorePurchasesButton
case secureConnectionButton
Expand Down
3 changes: 2 additions & 1 deletion ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions ios/MullvadVPN/View controllers/Account/AccountContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ 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)
button.setAccessibilityIdentifier(.storeKit2Button)
return button
}()

let redeemVoucherButton: AppButton = {
let button = AppButton(style: .success)
button.setAccessibilityIdentifier(.redeemVoucherButton)
Expand Down Expand Up @@ -85,6 +97,7 @@ class AccountContentView: UIView {
var arrangedSubviews = [UIView]()
#if DEBUG
arrangedSubviews.append(redeemVoucherButton)
arrangedSubviews.append(storeKit2Button)
#endif
arrangedSubviews.append(contentsOf: [
purchaseButton,
Expand Down
12 changes: 11 additions & 1 deletion ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -293,4 +296,65 @@ 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 {
// Accessing unsafe payload data because n
let receiptData = receipt.unsafePayloadValue
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion ios/MullvadVPN/View controllers/Account/PaymentState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down

0 comments on commit 911692b

Please sign in to comment.