1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
//
// SheetViewController.swift
// MullvadVPN
//
// Created by Steffen Ernst on 2025-01-29.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import StoreKit
import UIKit
class InAppPurchaseViewController: UIViewController, StorePaymentObserver {
private let storePaymentManager: StorePaymentManager
private let accountNumber: String
private let paymentAction: PaymentAction
private let errorPresenter: PaymentAlertPresenter
private let spinnerView = {
SpinnerActivityIndicatorView(style: .large)
}()
var didFinish: (() -> Void)?
init(
storePaymentManager: StorePaymentManager,
accountNumber: String,
errorPresenter: PaymentAlertPresenter,
paymentAction: PaymentAction
) {
self.storePaymentManager = storePaymentManager
self.accountNumber = accountNumber
self.errorPresenter = errorPresenter
self.paymentAction = paymentAction
super.init(nibName: nil, bundle: nil)
self.storePaymentManager.addPaymentObserver(self)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
view.addConstrainedSubviews([spinnerView]) {
spinnerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
spinnerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
}
view.backgroundColor = .black.withAlphaComponent(0.5)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
spinnerView.startAnimating()
let productIdentifiers = Set(StoreSubscription.allCases)
switch paymentAction {
case .purchase:
_ = storePaymentManager.requestProducts(
with: productIdentifiers
) { result in
Task { @MainActor [weak self] in
guard let self else { return }
self.spinnerView.stopAnimating()
switch result {
case let .success(success):
let products = success.products
guard !products.isEmpty else {
return
}
self.showPurchaseOptions(for: products)
case let .failure(failure as StorePaymentManagerError):
self.errorPresenter.showAlertForError(failure, context: .purchase) {
self.didFinish?()
}
case .failure:
self.didFinish?()
}
}
}
case .restorePurchase:
_ = storePaymentManager.restorePurchases(for: accountNumber) { result in
Task { @MainActor [weak self] in
guard let self else { return }
self.spinnerView.stopAnimating()
switch result {
case let .success(success):
self.errorPresenter.showAlertForResponse(success, context: .restoration) {
self.didFinish?()
}
case let .failure(failure as StorePaymentManagerError):
self.errorPresenter.showAlertForError(failure, context: .restoration) {
self.didFinish?()
}
case .failure:
self.didFinish?()
}
}
}
}
}
func purchase(product: SKProduct) {
let payment = SKPayment(product: product)
storePaymentManager.addPayment(payment, for: accountNumber)
}
func showPurchaseOptions(for products: [SKProduct]) {
let localizedString = NSLocalizedString("Add time", comment: "")
let sheetController = UIAlertController(
title: localizedString,
message: nil,
preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet
)
sheetController.overrideUserInterfaceStyle = .dark
sheetController.view.tintColor = .AlertController.tintColor
products.sortedByPrice().forEach { product in
guard let title = product.customLocalizedTitle else { return }
let action = UIAlertAction(
title: title, style: .default,
handler: { _ in
sheetController.dismiss(
animated: true,
completion: {
self.purchase(product: product)
self.spinnerView.startAnimating()
})
})
action
.accessibilityIdentifier = action.accessibilityIdentifier
sheetController.addAction(action)
}
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in
self.didFinish?()
}
cancelAction.accessibilityIdentifier = "actoin-sheet-cancel-button"
sheetController.addAction(cancelAction)
present(sheetController, animated: true)
}
nonisolated func storePaymentManager(_ manager: StorePaymentManager, didReceiveEvent event: StorePaymentEvent) {
Task { @MainActor in
spinnerView.stopAnimating()
switch event {
case let .finished(completion):
errorPresenter.showAlertForResponse(completion.serverResponse, context: .purchase) {
self.didFinish?()
}
case let .failure(paymentFailure):
switch paymentFailure.error {
case .storePayment(SKError.paymentCancelled):
self.didFinish?()
default:
errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) {
self.didFinish?()
}
}
}
}
}
}
|