Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StoreKit2 button #7400

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading