diff options
| author | Steffen Ernst <steffen@Steffens-MacBook-Pro.local> | 2025-01-13 14:01:24 +0100 |
|---|---|---|
| committer | Steffen Ernst <steffen.ernst@mullvad.net> | 2025-01-20 15:54:12 +0100 |
| commit | 7261493c00339a9945d4ac32161e8a245b975b6e (patch) | |
| tree | 3e90d636d06bfbe589df403f71d3eca22d75e843 | |
| parent | 9ce9f045d239d2863f1c7b75d4e9434cf1de8e8e (diff) | |
| download | mullvadvpn-7261493c00339a9945d4ac32161e8a245b975b6e.tar.xz mullvadvpn-7261493c00339a9945d4ac32161e8a245b975b6e.zip | |
Add 90 day payment option to our of time view
4 files changed, 115 insertions, 57 deletions
diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift index 54e928e726..354b4148d3 100644 --- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift @@ -8,6 +8,7 @@ import Routing import UIKit +import StoreKit class OutOfTimeCoordinator: Coordinator, Presenting, @preconcurrency OutOfTimeViewControllerDelegate, Poppable { let navigationController: RootContainerViewController @@ -82,4 +83,67 @@ class OutOfTimeCoordinator: Coordinator, Presenting, @preconcurrency OutOfTimeVi didFinishPayment?(self) } + + func outOfTimeViewControllerDidRequestShowPurchaseOptions(_ controller: OutOfTimeViewController, products: [SKProduct], didRequestPurchase: @escaping (SKProduct) -> Void) { + let localizedString = NSLocalizedString( + "BUY_CREDIT_BUTTON", + tableName: "Welcome", + value: "Add Time", + comment: "" + ) + let alert = UIAlertController(title: localizedString, message: nil, preferredStyle: .actionSheet) + products.forEach { product in + guard let localizedTitle = product.customLocalizedTitle else { + return + } + let action = UIAlertAction(title: localizedTitle, style: .default, handler: { _ in + alert.dismiss(animated: true, completion: { + didRequestPurchase(product) + }) + }) + action.accessibilityIdentifier = "\(AccessibilityIdentifier.purchaseButton.asString)_\(product.productIdentifier)" + alert.addAction(action) + } + let cancelAction = UIAlertAction(title: NSLocalizedString( + "PRODUCT_LIST_CANCEL_BUTTON", + tableName: "Welcome", + value: "Cancel", + comment: "" + ), style: .cancel) + cancelAction.accessibilityIdentifier = AccessibilityIdentifier.cancelPurchaseListButton.asString + alert.addAction(cancelAction) + 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: "" + ) + + 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 + ), + ] + ) + + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) + } } diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift index 00c9713fd1..194f2d5732 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift @@ -12,7 +12,6 @@ import UIKit protocol WelcomeViewControllerDelegate: AnyObject { func didRequestToRedeemVoucher(controller: WelcomeViewController) func didRequestToShowInfo(controller: WelcomeViewController) -// func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) func didRequestToViewPurchaseOptions(controller: WelcomeViewController, availableProducts: [SKProduct], accountNumber: String) func didRequestToShowFailToFetchProducts(controller: WelcomeViewController) } @@ -61,17 +60,6 @@ class WelcomeViewController: UIViewController, RootContainment { super.viewDidLoad() configureUI() contentView.viewModel = interactor.viewModel -// interactor.didChangeInAppPurchaseState = { [weak self] productState in -// guard let self else { return } -// switch productState { -// case .received(let products): -// delegate?.didRequestToViewPurchaseOptions(controller: self, availableProducts: products, accountNumber: interactor.accountNumber) -// case .failed: -// delegate?.didRequestToShowFailToFetchProducts(controller: self) -// default: break} -// -// self.contentView.productState = productState -// } interactor.viewDidLoad = true } diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift index 4955741292..71b71a418c 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift @@ -56,7 +56,7 @@ class OutOfTimeContentView: UIView { let localizedString = NSLocalizedString( "OUT_OF_TIME_PURCHASE_BUTTON", tableName: "OutOfTime", - value: "Add 30 days time", + value: "Add time", comment: "" ) button.setTitle(localizedString, for: .normal) diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift index 02dee6b9b5..56d5af7490 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift @@ -15,6 +15,8 @@ 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) } @MainActor @@ -32,7 +34,9 @@ class OutOfTimeViewController: UIViewController, RootContainment { } private lazy var contentView = OutOfTimeContentView() - + + private var isFetchingProducts = false + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } @@ -78,11 +82,11 @@ class OutOfTimeViewController: UIViewController, RootContainment { action: #selector(handleDisconnect(_:)), for: .touchUpInside ) -// contentView.purchaseButton.addTarget( -// self, -// action: #selector(doPurchase), -// for: .touchUpInside -// ) + contentView.purchaseButton.addTarget( + self, + action: #selector(requestStoreProducts), + for: .touchUpInside + ) contentView.restoreButton.addTarget( self, action: #selector(restorePurchases), @@ -101,13 +105,7 @@ class OutOfTimeViewController: UIViewController, RootContainment { self?.applyViewState() } } - - if StorePaymentManager.canMakePayments { - requestStoreProducts() - } else { - // Show popup -// productState = .cannotMakePurchases - } + applyViewState() } override func viewDidAppear(_ animated: Bool) { @@ -122,19 +120,6 @@ class OutOfTimeViewController: UIViewController, RootContainment { // MARK: - Private - private func requestStoreProducts() { -// let productIdentifiers = Set(StoreSubscription.allCases) -// -// productState = .fetching(productIdentifiers) -// -// _ = interactor.requestProducts(with: productIdentifiers) { [weak self] completion in -// let productState: ProductState = completion.value?.products -// .map { .received($0) } ?? .failed -// -// self?.productState = productState -// } - } - private func applyViewState() { let tunnelState = interactor.tunnelStatus.state let isInteractionEnabled = paymentState.allowsViewInteraction @@ -142,12 +127,10 @@ class OutOfTimeViewController: UIViewController, RootContainment { let isOutOfTime = interactor.deviceState.accountData.map { $0.expiry < Date() } ?? false -// purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) - // do this at the appropriate position -// contentView.purchaseButton.isLoading = productState.isFetching + contentView.purchaseButton.isLoading = isFetchingProducts -// purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled && !tunnelState -// .isSecured + purchaseButton.isEnabled = !isFetchingProducts && isInteractionEnabled && !tunnelState + .isSecured contentView.restoreButton.isEnabled = isInteractionEnabled contentView.enableDisconnectButton(tunnelState.isSecured, animated: true) @@ -171,7 +154,7 @@ class OutOfTimeViewController: UIViewController, RootContainment { tableName: "OutOfTime", value: """ You have no more VPN time left on this account. Either buy credit on our website \ - or make an in-app purchase via the **Add 30 days time** button below. + or make an in-app purchase via the **Add time** button below. """, comment: "" ) @@ -222,21 +205,44 @@ class OutOfTimeViewController: UIViewController, RootContainment { 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 doPurchase() { -// guard case let .received(product) = productState, -// let accountData = interactor.deviceState.accountData -// else { -// return -// } -// -// let payment = SKPayment(product: product) -// interactor.addPayment(payment, for: accountData.number) -// -// paymentState = .makingPayment(payment) -// } + @objc private func requestStoreProducts() { + guard let accountData = interactor.deviceState.accountData else { + return + } + let productIdentifiers = Set(StoreSubscription.allCases) + isFetchingProducts = true + applyViewState() + _ = interactor.requestProducts(with: productIdentifiers) { [weak self] result in + guard let self else { return } + switch result { + case .success(let 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() + } + } @objc func restorePurchases() { guard let accountData = interactor.deviceState.accountData else { |
