diff options
| author | Steffen Ernst <steffen.ernst@mullvad.net> | 2025-01-29 11:38:13 +0100 |
|---|---|---|
| committer | Steffen Ernst <steffen.ernst@mullvad.net> | 2025-02-17 09:40:33 +0100 |
| commit | aec878740e70fc1d0184a84ef2a7e80a84f2152c (patch) | |
| tree | a9225215ca27ea9fe2657bf35d5f84a1ee1334e6 /ios | |
| parent | 083d06db318ebdcb670d5b1ad5662a4c9df417c2 (diff) | |
| download | mullvadvpn-aec878740e70fc1d0184a84ef2a7e80a84f2152c.tar.xz mullvadvpn-aec878740e70fc1d0184a84ef2a7e80a84f2152c.zip | |
Use overlaying spinner for in app purchase
Move purchase logic into single reused view controller
Diffstat (limited to 'ios')
14 files changed, 281 insertions, 529 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index f79fb41659..e13e1af95a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -599,7 +599,6 @@ 7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */; }; 7A9CCCB92A96302800DD6A34 /* LocationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA72A96302700DD6A34 /* LocationCoordinator.swift */; }; 7A9CCCBA2A96302800DD6A34 /* CreateAccountVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */; }; - 7A9CCCBB2A96302800DD6A34 /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */; }; 7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */; }; 7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */; }; 7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */; }; @@ -1065,8 +1064,10 @@ F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; - F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; }; + F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */; }; + F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */; }; F95C1C252D3E5E8E00EBE769 /* UIAlertController+InAppPurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */; }; + F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; }; F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; F998EFFA2D3656BA00D88D01 /* SKProduct+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */; }; /* End PBXBuildFile section */ @@ -2000,7 +2001,6 @@ 7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedCoordinator.swift; sourceTree = "<group>"; }; 7A9CCCA72A96302700DD6A34 /* LocationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCoordinator.swift; sourceTree = "<group>"; }; 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountVoucherCoordinator.swift; sourceTree = "<group>"; }; - 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; }; 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogCoordinator.swift; sourceTree = "<group>"; }; 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = "<group>"; }; 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionCoordinator.swift; sourceTree = "<group>"; }; @@ -2335,6 +2335,8 @@ F0FBD98E2C4A60CC00EE5323 /* KeyExchangingResultStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyExchangingResultStub.swift; sourceTree = "<group>"; }; F910A4002D3FF22E002FF3BB /* View+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Modifier.swift"; sourceTree = "<group>"; }; F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+InAppPurchase.swift"; sourceTree = "<group>"; }; + F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; }; + F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseViewController.swift; sourceTree = "<group>"; }; F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -2947,6 +2949,7 @@ 583FE01629C196E8006E85F9 /* View controllers */ = { isa = PBXGroup; children = ( + F910A4322D4A1BA1002FF3BB /* InAppPurchase */, 583FE02029C1A0B1006E85F9 /* Account */, F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */, 7A2960F72A964A3500389B82 /* Alert */, @@ -3183,7 +3186,6 @@ E158B35F285381C60002F069 /* String+AccountFormatting.swift */, 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */, 5807E2BF2432038B00F5FF30 /* String+Helpers.swift */, - F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */, 58CEB2F82AFD136E00E6E088 /* UIBackgroundConfiguration+Extensions.swift */, 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, @@ -3644,6 +3646,7 @@ 58CAF9F22983D32200BE19F7 /* Coordinators */ = { isa = PBXGroup; children = ( + F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */, 7A6389D12B7E3BD6008E77E1 /* CustomLists */, 58EFC76F2AFB3FA800E9F4CB /* Settings */, 7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */, @@ -3653,7 +3656,6 @@ 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */, 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */, 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */, - 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */, 7A9CCCA72A96302700DD6A34 /* LocationCoordinator.swift */, 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */, 7A9CCCA42A96302700DD6A34 /* OutOfTimeCoordinator.swift */, @@ -4604,6 +4606,14 @@ path = ChangeLog; sourceTree = "<group>"; }; + F910A4322D4A1BA1002FF3BB /* InAppPurchase */ = { + isa = PBXGroup; + children = ( + F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */, + ); + path = InAppPurchase; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -6105,7 +6115,6 @@ 7A58699F2B50057100640D27 /* AccessMethodKind.swift in Sources */, 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */, 7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */, - 7A9CCCBB2A96302800DD6A34 /* InAppPurchaseCoordinator.swift in Sources */, 58CCA01822426713004F3011 /* AccountViewController.swift in Sources */, 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */, F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */, @@ -6192,6 +6201,7 @@ 5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */, 588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */, F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */, + F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */, 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, 7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */, 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */, @@ -6213,7 +6223,6 @@ A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */, 58CEB2E92AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift in Sources */, 58DFF7D02B02560400F864E0 /* NSAttributedString+Extensions.swift in Sources */, - F95C1C252D3E5E8E00EBE769 /* UIAlertController+InAppPurchase.swift in Sources */, 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, 7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, @@ -6294,6 +6303,7 @@ 586C0D812B03CA8400E7CDD7 /* CurrentValueSubject+UIActionBindings.swift in Sources */, 581DFAEA2B176C51005D6D1C /* PersistentProxyConfiguration+ViewModel.swift in Sources */, A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */, + F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */, F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index ed72e08593..c898950dd7 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -23,6 +23,7 @@ enum AddedMoreCreditOption: Equatable, Sendable { final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked Sendable { private let interactor: AccountInteractor + private let storePaymentManager: StorePaymentManager private var accountController: AccountViewController? let navigationController: UINavigationController @@ -34,10 +35,12 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked init( navigationController: UINavigationController, - interactor: AccountInteractor + interactor: AccountInteractor, + storePaymentManager: StorePaymentManager ) { self.navigationController = navigationController self.interactor = interactor + self.storePaymentManager = storePaymentManager } func start(animated: Bool) { @@ -68,24 +71,41 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked navigateToDeleteAccount() case .restorePurchasesInfo: showRestorePurchasesInfo() - case let .showPurchaseOptions(details): - showPurchaseOptions( - products: details.products, - accountNumber: details.accountNumber, - didRequestPurchase: details.didRequestPurchase - ) case .showFailedToLoadProducts: showFailToFetchProducts() + case .showRestorePurchases: + showRestorePurchases() + case .showPurchaseOptions: + showPurchaseOptions() } } - func showPurchaseOptions( - products: [SKProduct], - accountNumber: String, - didRequestPurchase: @escaping (_ product: SKProduct) -> Void - ) { - let alert = UIAlertController.showInAppPurchaseAlert(products: products, didRequestPurchase: didRequestPurchase) - presentationContext.present(alert, animated: true) + private func showPurchaseOptions() { + guard let accountNumber = interactor.deviceState.accountData?.number else { return } + let coordinator = InAppPurchaseCoordinator( + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + paymentAction: .purchase + ) + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) + } + coordinator.start() + presentChild(coordinator, animated: true) + } + + private func showRestorePurchases() { + guard let accountNumber = interactor.deviceState.accountData?.number else { return } + let coordinator = InAppPurchaseCoordinator( + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + paymentAction: .restorePurchase + ) + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) + } + coordinator.start() + presentChild(coordinator, animated: true) } private func navigateToRedeemVoucher() { diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index dbea333624..f8b6e7a555 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -524,7 +524,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo private func presentAccount(animated: Bool, completion: @escaping (Coordinator) -> Void) { let accountInteractor = AccountInteractor( - storePaymentManager: storePaymentManager, tunnelManager: tunnelManager, accountsProxy: accountsProxy, apiProxy: apiProxy @@ -532,7 +531,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo let coordinator = AccountCoordinator( navigationController: CustomNavigationController(), - interactor: accountInteractor + interactor: accountInteractor, + storePaymentManager: storePaymentManager ) coordinator.didFinish = { [weak self] _, reason in @@ -557,7 +557,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo completion: @escaping @Sendable (Coordinator) -> Void ) { let interactorFactory = SettingsInteractorFactory( - storePaymentManager: storePaymentManager, tunnelManager: tunnelManager, apiProxy: apiProxy, relayCacheTracker: relayCacheTracker, diff --git a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift index 97a72a8ce6..ac8c6413f6 100644 --- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift @@ -6,73 +6,44 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import Foundation import Routing import StoreKit import UIKit -class InAppPurchaseCoordinator: Coordinator, Presenting, Presentable { - private let navigationController: RootContainerViewController - private let interactor: InAppPurchaseInteractor +enum PaymentAction { + case purchase + case restorePurchase +} + +final class InAppPurchaseCoordinator: Coordinator, Presentable, Presenting { + private var sheetController: InAppPurchaseViewController? + private let storePaymentManager: StorePaymentManager + private let accountNumber: String + private let paymentAction: PaymentAction var didFinish: ((InAppPurchaseCoordinator) -> Void)? - var didCancel: ((InAppPurchaseCoordinator) -> Void)? var presentedViewController: UIViewController { - navigationController + return sheetController! } - init(navigationController: RootContainerViewController, interactor: InAppPurchaseInteractor) { - self.navigationController = navigationController - self.interactor = interactor + init(storePaymentManager: StorePaymentManager, accountNumber: String, paymentAction: PaymentAction) { + self.storePaymentManager = storePaymentManager + self.accountNumber = accountNumber + self.paymentAction = paymentAction } - func start(accountNumber: String, product: SKProduct) { - interactor.purchase(accountNumber: accountNumber, product: product) - interactor.didFinishPayment = { [weak self] _, paymentEvent in - guard let self else { return } - switch paymentEvent { - case let .finished(value): - let coordinator = AddCreditSucceededCoordinator( - purchaseType: .inAppPurchase, - timeAdded: Int(value.serverResponse.timeAdded), - navigationController: navigationController - ) - - coordinator.didFinish = { [weak self] coordinator in - coordinator.removeFromParent() - guard let self else { return } - didFinish?(self) - } - - addChild(coordinator) - coordinator.start() - - case let .failure(failure): - let presentation = AlertPresentation( - id: "in-app-purchase-error-alert", - icon: .alert, - message: failure.error.localizedDescription, - buttons: [ - AlertAction( - title: NSLocalizedString( - "IN_APP_PURCHASE_ERROR_DIALOG_OK_ACTION", - tableName: "Welcome", - value: "Got it!", - comment: "" - ), - style: .default, - handler: { [weak self] in - guard let self = self else { return } - self.didCancel?(self) - } - ), - ] - ) + func dismiss() { + didFinish?(self) + } - let presenter = AlertPresenter(context: self) - presenter.showAlert(presentation: presentation, animated: true) - } - } + func start() { + sheetController = InAppPurchaseViewController( + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + errorPresenter: PaymentAlertPresenter(alertContext: self), + paymentAction: paymentAction + ) + sheetController?.didFinish = dismiss } } diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift index 18e042d187..8930cc2242 100644 --- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift @@ -74,54 +74,29 @@ class OutOfTimeCoordinator: Coordinator, Presenting, @preconcurrency OutOfTimeVi // MARK: - OutOfTimeViewControllerDelegate - func outOfTimeViewControllerDidBeginPayment(_ controller: OutOfTimeViewController) { - isMakingPayment = true - } - - func outOfTimeViewControllerDidEndPayment(_ controller: OutOfTimeViewController) { - isMakingPayment = false - - didFinishPayment?(self) - } - - func outOfTimeViewControllerDidRequestShowPurchaseOptions( - _ controller: OutOfTimeViewController, - products: [SKProduct], - didRequestPurchase: @escaping (SKProduct) -> Void - ) { - let alert = UIAlertController.showInAppPurchaseAlert(products: products, didRequestPurchase: didRequestPurchase) - presentationContext.present(alert, animated: true) - } - - func outOfTimeViewControllerDidFailToFetchProducts(_ controller: OutOfTimeViewController) { - let message = NSLocalizedString( - "WELCOME_FAILED_TO_FETCH_PRODUCTS_DIALOG", - tableName: "Welcome", - value: - """ - Failed to connect to App store, please try again later. - """, - comment: "" + func didRequestShowPurchaseOptions(accountNumber: String) { + let coordinator = InAppPurchaseCoordinator( + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + paymentAction: .purchase ) + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) + } + coordinator.start() + presentChild(coordinator, animated: true) + } - let presentation = AlertPresentation( - id: "welcome-failed-to-fetch-products-alert", - icon: .info, - message: message, - buttons: [ - AlertAction( - title: NSLocalizedString( - "WELCOME_FAILED_TO_FETCH_PRODUCTS_OK_ACTION", - tableName: "Welcome", - value: "Got it!", - comment: "" - ), - style: .default - ), - ] + func didRequestShowRestorePurchase(accountNumber: String) { + let coordinator = InAppPurchaseCoordinator( + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + paymentAction: .restorePurchase ) - - let presenter = AlertPresenter(context: self) - presenter.showAlert(presentation: presentation, animated: true) + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) + } + coordinator.start() + presentChild(coordinator, animated: true) } } diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index d48825d07c..fec5a6f1d7 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -151,46 +151,18 @@ extension WelcomeCoordinator: @preconcurrency WelcomeViewControllerDelegate { } func didRequestToViewPurchaseOptions( - controller: WelcomeViewController, - products: [SKProduct], accountNumber: String ) { - let alert = UIAlertController.showInAppPurchaseAlert(products: products, didRequestPurchase: { product in - self.didRequestToPurchaseCredit( - controller: controller, - accountNumber: accountNumber, - product: product - ) - }) - - presentationContext.present(alert, animated: true) - } - - func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) { - navigationController.enableHeaderBarButtons(false) - let coordinator = InAppPurchaseCoordinator( - navigationController: navigationController, - interactor: inAppPurchaseInteractor + storePaymentManager: storePaymentManager, + accountNumber: accountNumber, + paymentAction: .purchase ) - - inAppPurchaseInteractor.viewControllerDelegate = viewController - - coordinator.didFinish = { [weak self] coordinator in - guard let self else { return } - navigationController.enableHeaderBarButtons(true) - coordinator.removeFromParent() - didFinish?() - } - - coordinator.didCancel = { [weak self] coordinator in - self?.navigationController.enableHeaderBarButtons(true) - coordinator.removeFromParent() + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) } - - addChild(coordinator) - - coordinator.start(accountNumber: accountNumber, product: product) + coordinator.start() + presentChild(coordinator, animated: true) } func didRequestToRedeemVoucher(controller: WelcomeViewController) { diff --git a/ios/MullvadVPN/Extensions/UIAlertController+InAppPurchase.swift b/ios/MullvadVPN/Extensions/UIAlertController+InAppPurchase.swift deleted file mode 100644 index c7096d91f0..0000000000 --- a/ios/MullvadVPN/Extensions/UIAlertController+InAppPurchase.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// UIAlertController+InAppPurchase.swift -// MullvadVPN -// -// Created by Steffen Ernst on 2025-01-20. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import StoreKit -import UIKit - -extension UIAlertController { - public static func showInAppPurchaseAlert( - products: [SKProduct], - didRequestPurchase: @escaping (SKProduct) -> Void - ) -> UIAlertController { - let localizedString = NSLocalizedString( - "ADD_TIME_BUTTON", - tableName: "Welcome", - value: "Add Time", - comment: "" - ) - let actionSheet = UIAlertController( - title: localizedString, - message: nil, - preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet - ) - actionSheet.overrideUserInterfaceStyle = .dark - actionSheet.view.tintColor = .AlertController.tintColor - products.sortedByPrice().forEach { product in - guard let localizedTitle = product.customLocalizedTitle else { - return - } - let action = UIAlertAction(title: localizedTitle, style: .default, handler: { _ in - actionSheet.dismiss(animated: true, completion: { - didRequestPurchase(product) - }) - }) - action - .accessibilityIdentifier = - "\(AccessibilityIdentifier.purchaseButton.asString)_\(product.productIdentifier)" - actionSheet.addAction(action) - } - let cancelAction = UIAlertAction(title: NSLocalizedString( - "PRODUCT_LIST_CANCEL_BUTTON", - tableName: "Welcome", - value: "Cancel", - comment: "" - ), style: .cancel) - cancelAction.accessibilityIdentifier = AccessibilityIdentifier.cancelPurchaseListButton.asString - actionSheet.addAction(cancelAction) - return actionSheet - } -} diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index 85db6f7935..a9e6aec2d1 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -14,24 +14,20 @@ import Operations import StoreKit final class AccountInteractor: Sendable { - private let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager let accountsProxy: RESTAccountHandling let apiProxy: APIQuerying - nonisolated(unsafe) var didReceivePaymentEvent: (@Sendable (StorePaymentEvent) -> Void)? nonisolated(unsafe) var didReceiveDeviceState: (@Sendable (DeviceState) -> Void)? nonisolated(unsafe) private var tunnelObserver: TunnelObserver? nonisolated(unsafe) private var paymentObserver: StorePaymentObserver? init( - storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, accountsProxy: RESTAccountHandling, apiProxy: APIQuerying ) { - self.storePaymentManager = storePaymentManager self.tunnelManager = tunnelManager self.accountsProxy = accountsProxy self.apiProxy = apiProxy @@ -41,15 +37,9 @@ final class AccountInteractor: Sendable { self?.didReceiveDeviceState?(deviceState) }) - let paymentObserver = StorePaymentBlockObserver { [weak self] _, event in - self?.didReceivePaymentEvent?(event) - } - tunnelManager.addObserver(tunnelObserver) - storePaymentManager.addPaymentObserver(paymentObserver) self.tunnelObserver = tunnelObserver - self.paymentObserver = paymentObserver } var deviceState: DeviceState { @@ -60,34 +50,10 @@ final class AccountInteractor: Sendable { await tunnelManager.unsetAccount() } - func addPayment(_ payment: SKPayment, for accountNumber: String) { - 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 @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void - ) -> Cancellable { - storePaymentManager.restorePurchases( - for: accountNumber, - completionHandler: completionHandler - ) - } - - func requestProducts( - with productIdentifiers: Set<StoreSubscription>, - completionHandler: @escaping @Sendable (Result<SKProductsResponse, Error>) -> Void - ) -> Cancellable { - storePaymentManager.requestProducts( - with: productIdentifiers, - completionHandler: completionHandler - ) - } } diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 87a450339b..7ad04a083c 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -14,12 +14,6 @@ import Operations import StoreKit import UIKit -struct PurchaseOptionDetails: Sendable { - let products: [SKProduct] - let accountNumber: String - let didRequestPurchase: @Sendable (SKProduct) -> Void -} - enum AccountViewControllerAction: Sendable { case deviceInfo case finish @@ -27,8 +21,9 @@ enum AccountViewControllerAction: Sendable { case navigateToVoucher case navigateToDeleteAccount case restorePurchasesInfo - case showPurchaseOptions(PurchaseOptionDetails) + case showPurchaseOptions case showFailedToLoadProducts + case showRestorePurchases } class AccountViewController: UIViewController, @unchecked Sendable { @@ -104,11 +99,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { } } - interactor.didReceivePaymentEvent = { [weak self] event in - Task { @MainActor in - self?.didReceivePaymentEvent(event) - } - } configUI() addActions() updateView(from: interactor.deviceState) @@ -151,17 +141,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { contentView.storeKit2Button.addTarget(self, action: #selector(handleStoreKit2Purchase), for: .touchUpInside) } - private func doPurchase(product: SKProduct) { - guard let accountData = interactor.deviceState.accountData else { - return - } - - let payment = SKPayment(product: product) - interactor.addPayment(payment, for: accountData.number) - - setPaymentState(.makingPayment(payment), animated: true) - } - @MainActor private func setPaymentState(_ newState: PaymentState, animated: Bool) { paymentState = newState @@ -188,15 +167,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { private func applyViewState(animated: Bool) { let isInteractionEnabled = paymentState.allowsViewInteraction let purchaseButton = contentView.purchaseButton - let activityIndicator = contentView.accountExpiryRowView.activityIndicator - - if isFetchingProducts || paymentState != .none { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - - contentView.purchaseButton.isLoading = isFetchingProducts purchaseButton.isEnabled = !isFetchingProducts && isInteractionEnabled contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled) @@ -214,27 +184,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { navigationItem.setHidesBackButton(!isInteractionEnabled, animated: animated) } - private func didReceivePaymentEvent(_ event: StorePaymentEvent) { - guard case let .makingPayment(payment) = paymentState, - payment == event.payment else { return } - - switch event { - case let .finished(completion): - errorPresenter.showAlertForResponse(completion.serverResponse, context: .purchase) - - case let .failure(paymentFailure): - switch paymentFailure.error { - case .storePayment(SKError.paymentCancelled): - break - - default: - errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) - } - } - - setPaymentState(.none, animated: true) - } - private func copyAccountToken() { guard let accountData = interactor.deviceState.accountData else { return @@ -262,58 +211,11 @@ class AccountViewController: UIViewController, @unchecked Sendable { } @objc private func requestStoreProducts() { - guard let accountData = interactor.deviceState.accountData else { - return - } - let productIdentifiers = Set(StoreSubscription.allCases) - setIsFetchingProducts(true) - _ = interactor.requestProducts(with: productIdentifiers) { [weak self] result in - guard let self else { return } - Task { @MainActor in - switch result { - case let .success(success): - let products = success.products - if !products.isEmpty { - actionHandler?(.showPurchaseOptions(PurchaseOptionDetails( - products: products, - accountNumber: accountData.number, - didRequestPurchase: { product in Task { @MainActor in self.doPurchase(product: product) }} - ))) - } else { - actionHandler?(.showFailedToLoadProducts) - } - case .failure: - actionHandler?(.showFailedToLoadProducts) - } - setIsFetchingProducts(false) - } - } + actionHandler?(.showPurchaseOptions) } @objc private func restorePurchases() { - guard let accountData = interactor.deviceState.accountData else { - return - } - - setPaymentState(.restoringPurchases, animated: true) - _ = interactor.restorePurchases(for: accountData.number) { [weak self] completion in - guard let self else { return } - - Task { @MainActor in - switch completion { - case let .success(response): - errorPresenter.showAlertForResponse(response, context: .restoration) - - case let .failure(error as StorePaymentManagerError): - errorPresenter.showAlertForError(error, context: .restoration) - - default: - break - } - - setPaymentState(.none, animated: true) - } - } + actionHandler?(.showRestorePurchases) } @objc private func handleStoreKit2Purchase() { diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift index 13dab539d4..ba05410b67 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift @@ -174,7 +174,6 @@ final class WelcomeContentView: UIView, Sendable { var isPurchasing = false { didSet { let alpha = isPurchasing ? 0.7 : 1.0 - purchaseButton.isLoading = isPurchasing purchaseButton.isEnabled = !isPurchasing purchaseButton.alpha = alpha redeemVoucherButton.isEnabled = !isPurchasing diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift index 7d4ca9f6b5..a94c2ab8c0 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift @@ -13,11 +13,8 @@ protocol WelcomeViewControllerDelegate: AnyObject { func didRequestToRedeemVoucher(controller: WelcomeViewController) func didRequestToShowInfo(controller: WelcomeViewController) func didRequestToViewPurchaseOptions( - controller: WelcomeViewController, - products: [SKProduct], accountNumber: String ) - func didRequestToShowFailToFetchProducts(controller: WelcomeViewController) } class WelcomeViewController: UIViewController, RootContainment { @@ -89,42 +86,10 @@ extension WelcomeViewController: @preconcurrency WelcomeContentViewDelegate { } func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) { - let productIdentifiers = Set(StoreSubscription.allCases) - contentView.isFetchingProducts = true - _ = interactor.requestProducts(with: productIdentifiers) { [weak self] result in - guard let self else { return } - Task { @MainActor in - switch result { - case let .success(success): - let products = success.products - if !products.isEmpty { - delegate?.didRequestToViewPurchaseOptions( - controller: self, - products: products, - accountNumber: interactor.accountNumber - ) - } else { - delegate?.didRequestToShowFailToFetchProducts(controller: self) - } - case .failure: - delegate?.didRequestToShowFailToFetchProducts(controller: self) - } - contentView.isFetchingProducts = false - } - } + delegate?.didRequestToViewPurchaseOptions(accountNumber: interactor.accountNumber) } func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton) { delegate?.didRequestToRedeemVoucher(controller: self) } } - -extension WelcomeViewController: @preconcurrency InAppPurchaseViewControllerDelegate { - func didBeginPayment() { - contentView.isPurchasing = true - } - - func didEndPayment() { - contentView.isPurchasing = false - } -} diff --git a/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift b/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift new file mode 100644 index 0000000000..ec52a4111f --- /dev/null +++ b/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift @@ -0,0 +1,163 @@ +// +// 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, @preconcurrency 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?() + } + default: + 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?() + } + default: + 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_BUTTON", + tableName: "Welcome", + value: "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( + "SHEET_CANCEL_BUTTON", + tableName: "ActionSheet", + value: "Cancel", + comment: "" + ), style: .cancel) { _ in + self.didFinish?() + } + cancelAction.accessibilityIdentifier = "actoin-sheet-cancel-button" + sheetController.addAction(cancelAction) + present(sheetController, animated: true) + } + + @MainActor + func storePaymentManager(_ manager: StorePaymentManager, didReceiveEvent event: StorePaymentEvent) { + 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?() + } + } + } + } +} diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift index 36359599e5..07dc4bda56 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift @@ -13,14 +13,8 @@ import Operations import UIKit protocol OutOfTimeViewControllerDelegate: AnyObject, Sendable { - func outOfTimeViewControllerDidBeginPayment(_ controller: OutOfTimeViewController) - func outOfTimeViewControllerDidEndPayment(_ controller: OutOfTimeViewController) - func outOfTimeViewControllerDidRequestShowPurchaseOptions( - _ controller: OutOfTimeViewController, - products: [SKProduct], - didRequestPurchase: @escaping (SKProduct) -> Void - ) - func outOfTimeViewControllerDidFailToFetchProducts(_ controller: OutOfTimeViewController) + func didRequestShowPurchaseOptions(accountNumber: String) + func didRequestShowRestorePurchase(accountNumber: String) } @MainActor @@ -28,19 +22,9 @@ class OutOfTimeViewController: UIViewController, RootContainment { weak var delegate: OutOfTimeViewControllerDelegate? private let interactor: OutOfTimeInteractor - private let errorPresenter: PaymentAlertPresenter - - private var paymentState: PaymentState = .none { - didSet { - applyViewState() - notifyDelegate(oldValue) - } - } private lazy var contentView = OutOfTimeContentView() - private var isFetchingProducts = false - override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } @@ -64,7 +48,6 @@ class OutOfTimeViewController: UIViewController, RootContainment { init(interactor: OutOfTimeInteractor, errorPresenter: PaymentAlertPresenter) { self.interactor = interactor - self.errorPresenter = errorPresenter super.init(nibName: nil, bundle: nil) } @@ -101,12 +84,6 @@ class OutOfTimeViewController: UIViewController, RootContainment { for: .touchUpInside ) - interactor.didReceivePaymentEvent = { [weak self] event in - Task { @MainActor in - self?.didReceivePaymentEvent(event) - } - } - interactor.didReceiveTunnelStatus = { [weak self] _ in Task { @MainActor in self?.setNeedsHeaderBarStyleAppearanceUpdate() @@ -130,17 +107,9 @@ class OutOfTimeViewController: UIViewController, RootContainment { private func applyViewState() { let tunnelState = interactor.tunnelStatus.state - let isInteractionEnabled = paymentState.allowsViewInteraction let purchaseButton = contentView.purchaseButton let isOutOfTime = interactor.deviceState.accountData.map { $0.expiry < Date() } ?? false - - contentView.purchaseButton.isLoading = isFetchingProducts - - purchaseButton.isEnabled = !isFetchingProducts && isInteractionEnabled && !tunnelState - .isSecured - contentView.restoreButton.isEnabled = isInteractionEnabled - contentView.enableDisconnectButton(tunnelState.isSecured, animated: true) if tunnelState.isSecured { @@ -168,124 +137,22 @@ class OutOfTimeViewController: UIViewController, RootContainment { ) ) } - - if !isInteractionEnabled { - contentView.statusActivityView.state = .activity - } else { - contentView.statusActivityView.state = isOutOfTime ? .failure : .success - } - - view.isUserInteractionEnabled = isInteractionEnabled - } - - private func notifyDelegate(_ oldPaymentState: PaymentState) { - switch (oldPaymentState, paymentState) { - case (.none, .makingPayment), (.none, .restoringPurchases): - delegate?.outOfTimeViewControllerDidBeginPayment(self) - - case (.makingPayment, .none), (.restoringPurchases, .none): - delegate?.outOfTimeViewControllerDidEndPayment(self) - - default: - break - } - } - - private func didReceivePaymentEvent(_ event: StorePaymentEvent) { - guard case let .makingPayment(payment) = paymentState, - payment == event.payment else { return } - - switch event { - case let .finished(completion): - errorPresenter.showAlertForResponse(completion.serverResponse, context: .purchase) - - case let .failure(paymentFailure): - switch paymentFailure.error { - case .storePayment(SKError.paymentCancelled): - break - - default: - errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) { - self.paymentState = .none - } - } - } - - paymentState = .none - } - - private func doPurchase(product: SKProduct) { - guard let accountData = interactor.deviceState.accountData else { - return - } - - let payment = SKPayment(product: product) - interactor.addPayment(payment, for: accountData.number) - - paymentState = .makingPayment(payment) } // MARK: - Actions @objc private func requestStoreProducts() { - guard interactor.deviceState.accountData != nil else { + guard let accountNumber = interactor.deviceState.accountData?.number else { return } - let productIdentifiers = Set(StoreSubscription.allCases) - isFetchingProducts = true - applyViewState() - _ = interactor.requestProducts(with: productIdentifiers) { [weak self] result in - guard let self else { return } - Task { @MainActor in - switch result { - case let .success(success): - let products = success.products - if !products.isEmpty { - delegate?.outOfTimeViewControllerDidRequestShowPurchaseOptions( - self, - products: products, - didRequestPurchase: self.doPurchase - ) - } else { - delegate?.outOfTimeViewControllerDidFailToFetchProducts(self) - } - case .failure: - delegate?.outOfTimeViewControllerDidFailToFetchProducts(self) - } - isFetchingProducts = false - applyViewState() - } - } + delegate?.didRequestShowPurchaseOptions(accountNumber: accountNumber) } @objc func restorePurchases() { - guard let accountData = interactor.deviceState.accountData else { + guard let accountNumber = interactor.deviceState.accountData?.number else { return } - - paymentState = .restoringPurchases - - /// Safe to assume `@MainActor` isolation because `SendStoreReceiptOperation` sets both its - /// `dispatchQueue` and `completionQueue` to `.main` - _ = interactor.restorePurchases(for: accountData.number) { [weak self] result in - guard let self else { return } - MainActor.assumeIsolated { - switch result { - case let .success(response): - errorPresenter.showAlertForResponse(response, context: .restoration) { - self.paymentState = .none - } - - case let .failure(error as StorePaymentManagerError): - errorPresenter.showAlertForError(error, context: .restoration) { - self.paymentState = .none - } - - default: - paymentState = .none - } - } - } + delegate?.didRequestShowRestorePurchase(accountNumber: accountNumber) } @objc private func handleDisconnect(_ sender: Any) { diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift index 375629a90e..b0b106ab1c 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift @@ -10,7 +10,6 @@ import MullvadREST import MullvadSettings final class SettingsInteractorFactory { - private let storePaymentManager: StorePaymentManager private let apiProxy: APIQuerying private let relayCacheTracker: RelayCacheTracker private let ipOverrideRepository: IPOverrideRepositoryProtocol @@ -18,13 +17,11 @@ final class SettingsInteractorFactory { let tunnelManager: TunnelManager init( - storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, apiProxy: APIQuerying, relayCacheTracker: RelayCacheTracker, ipOverrideRepository: IPOverrideRepositoryProtocol ) { - self.storePaymentManager = storePaymentManager self.tunnelManager = tunnelManager self.apiProxy = apiProxy self.relayCacheTracker = relayCacheTracker |
