diff options
| author | Steffen Ernst <steffen@Steffens-MacBook-Pro.local> | 2025-01-13 10:41:56 +0100 |
|---|---|---|
| committer | Steffen Ernst <steffen.ernst@mullvad.net> | 2025-01-20 15:54:11 +0100 |
| commit | 426cc4241a91539361bdabd08958a5e04788dbf1 (patch) | |
| tree | 7506dde59ad557e06b6ef135fc13e99738e66e93 | |
| parent | abefbd407a6191a044fea4cc58e431118587fcd9 (diff) | |
| download | mullvadvpn-426cc4241a91539361bdabd08958a5e04788dbf1.tar.xz mullvadvpn-426cc4241a91539361bdabd08958a5e04788dbf1.zip | |
Add 90 day payment to welcome and account view
12 files changed, 279 insertions, 193 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 5af100575d..ab577c53a7 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -154,7 +154,6 @@ 58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */; }; 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58677711290976FB006F721F /* SettingsInteractor.swift */; }; 5867771429097BCD006F721F /* PaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771329097BCD006F721F /* PaymentState.swift */; }; - 5867771629097C5B006F721F /* ProductState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771529097C5B006F721F /* ProductState.swift */; }; 5868585524054096000B8131 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* CustomButton.swift */; }; 586A0DCB2A20E359006C731C /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; 586A0DD12A20E371006C731C /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 586A0DD02A20E371006C731C /* WireGuardKitTypes */; }; @@ -1613,7 +1612,6 @@ 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractorFactory.swift; sourceTree = "<group>"; }; 58677711290976FB006F721F /* SettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractor.swift; sourceTree = "<group>"; }; 5867771329097BCD006F721F /* PaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentState.swift; sourceTree = "<group>"; }; - 5867771529097C5B006F721F /* ProductState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductState.swift; sourceTree = "<group>"; }; 5868585424054096000B8131 /* CustomButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; }; 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = "<group>"; }; 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = "<group>"; }; @@ -3106,7 +3104,6 @@ 58CCA01722426713004F3011 /* AccountViewController.swift */, 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */, 5867771329097BCD006F721F /* PaymentState.swift */, - 5867771529097C5B006F721F /* ProductState.swift */, 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */, ); path = Account; @@ -6082,7 +6079,6 @@ 5868585524054096000B8131 /* CustomButton.swift in Sources */, 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, 7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */, - 5867771629097C5B006F721F /* ProductState.swift in Sources */, 7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */, 7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */, 58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 728684ff95..a2b2bc25f9 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -60,7 +60,7 @@ public enum AccessibilityIdentifier: Equatable { case selectLocationFilterButton case relayFilterChipCloseButton case openPortSelectorMenuButton - + case cancelPurchaseListButton // Cells case deviceCell case accessMethodProtocolSelectionCell diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index 50f2a54d46..28e39c831d 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -8,6 +8,7 @@ import Routing import UIKit +import StoreKit enum AccountDismissReason: Equatable, Sendable { case none @@ -67,8 +68,43 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked navigateToDeleteAccount() case .restorePurchasesInfo: showRestorePurchasesInfo() + case .showPurchaseOptions(let details): + showPurchaseOptions(availableProducts: details.products, accountNumber: details.accountNumber, didRequestPurchase: details.didRequestPurchase) + case .showFailedToLoadProducts: + showFailToFetchProducts() } } + + func showPurchaseOptions(availableProducts: [SKProduct], accountNumber: String, didRequestPurchase: @escaping (_ product: SKProduct) -> Void) { + let localizedString = NSLocalizedString( + "BUY_CREDIT_BUTTON", + tableName: "Welcome", + value: "Add Time", + comment: "" + ) + let alert = UIAlertController(title: localizedString, message: nil, preferredStyle: .actionSheet) + availableProducts.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) + } private func navigateToRedeemVoucher() { let coordinator = ProfileVoucherCoordinator( @@ -230,4 +266,37 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked let presenter = AlertPresenter(context: self) presenter.showAlert(presentation: presentation, animated: true) } + + + func showFailToFetchProducts() { + 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/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index 0f21f39f1c..90255d306e 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -79,8 +79,39 @@ final class WelcomeCoordinator: Coordinator, Poppable, Presenting { ) } } - extension WelcomeCoordinator: @preconcurrency WelcomeViewControllerDelegate { + func didRequestToShowFailToFetchProducts(controller: WelcomeViewController) { + 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) + } + func didRequestToShowInfo(controller: WelcomeViewController) { let message = NSLocalizedString( "WELCOME_DEVICE_CONCEPT_TEXT_DIALOG", @@ -117,6 +148,37 @@ extension WelcomeCoordinator: @preconcurrency WelcomeViewControllerDelegate { let presenter = AlertPresenter(context: self) presenter.showAlert(presentation: presentation, animated: true) } + + func didRequestToViewPurchaseOptions(controller: WelcomeViewController, availableProducts: [SKProduct], accountNumber: String) { + let localizedString = NSLocalizedString( + "BUY_CREDIT_BUTTON", + tableName: "Welcome", + value: "Add Time", + comment: "" + ) + let alert = UIAlertController(title: localizedString, message: nil, preferredStyle: .actionSheet) + availableProducts.forEach { product in + guard let localizedTitle = product.customLocalizedTitle else { + return + } + let action = UIAlertAction(title: localizedTitle, style: .default, handler: { _ in + alert.dismiss(animated: true, completion: { + self.didRequestToPurchaseCredit(controller: controller, accountNumber: accountNumber, product: 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 didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) { navigationController.enableHeaderBarButtons(false) diff --git a/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift b/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift index f20a2d4e2d..884da8ea87 100644 --- a/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift +++ b/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift @@ -9,9 +9,10 @@ import Foundation import StoreKit -enum StoreSubscription: String { +enum StoreSubscription: String, CaseIterable { /// Thirty days non-renewable subscription case thirtyDays = "net.mullvad.MullvadVPN.subscription.30days" + case ninetyDays = "net.mullvad.MullvadVPN.subscription.90days" var localizedTitle: String { switch self { @@ -19,7 +20,14 @@ enum StoreSubscription: String { return NSLocalizedString( "STORE_SUBSCRIPTION_TITLE_ADD_30_DAYS", tableName: "StoreSubscriptions", - value: "Add 30 days time", + value: "Add 30 days", + comment: "" + ) + case .ninetyDays: + return NSLocalizedString( + "STORE_SUBSCRIPTION_TITLE_ADD_90_DAYS", + tableName: "StoreSubscriptions", + value: "Add 90 days", comment: "" ) } @@ -28,7 +36,11 @@ enum StoreSubscription: String { extension SKProduct { var customLocalizedTitle: String? { - StoreSubscription(rawValue: productIdentifier)?.localizedTitle + guard let localizedTitle = StoreSubscription(rawValue: productIdentifier)?.localizedTitle, + let localizedPrice else { + return nil + } + return "\(localizedTitle) (\(localizedPrice))" } } diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift index bedcdbdf04..3d34c3df29 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift @@ -12,6 +12,12 @@ class AccountContentView: UIView { let purchaseButton: InAppPurchaseButton = { let button = InAppPurchaseButton() button.setAccessibilityIdentifier(.purchaseButton) + button.setTitle(NSLocalizedString( + "ADD_TIME_BUTTON_TITLE", + tableName: "Account", + value: "Add time", + comment: "" + ), for: .normal) return button }() diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 19060381ab..32a24c9699 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -14,6 +14,12 @@ import Operations import StoreKit import UIKit +struct PurchaseOptionDetails { + let products: [SKProduct] + let accountNumber: String + let didRequestPurchase: (SKProduct) -> Void +} + enum AccountViewControllerAction: Sendable { case deviceInfo case finish @@ -21,6 +27,8 @@ enum AccountViewControllerAction: Sendable { case navigateToVoucher case navigateToDeleteAccount case restorePurchasesInfo + case showPurchaseOptions(PurchaseOptionDetails) + case showFailedToLoadProducts } class AccountViewController: UIViewController, @unchecked Sendable { @@ -35,7 +43,7 @@ class AccountViewController: UIViewController, @unchecked Sendable { return contentView }() - private var productState: ProductState = .none + private var isFetchingProducts: Bool = false private var paymentState: PaymentState = .none var actionHandler: ActionHandler? @@ -105,19 +113,10 @@ class AccountViewController: UIViewController, @unchecked Sendable { addActions() updateView(from: interactor.deviceState) applyViewState(animated: false) - requestStoreProductsIfCan() } // MARK: - Private - private func requestStoreProductsIfCan() { - if StorePaymentManager.canMakePayments { - requestStoreProducts() - } else { - setProductState(.cannotMakePurchases, animated: false) - } - } - private func configUI() { let scrollView = UIScrollView() @@ -141,7 +140,7 @@ class AccountViewController: UIViewController, @unchecked Sendable { contentView.purchaseButton.addTarget( self, - action: #selector(doPurchase), + action: #selector(requestStoreProducts), for: .touchUpInside ) @@ -151,22 +150,16 @@ 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 + } - private func requestStoreProducts() { - let productKind = StoreSubscription.thirtyDays - - setProductState(.fetching(productKind), animated: true) - - _ = interactor.requestProducts(with: [productKind]) { [weak self] completion in - let productState: ProductState = completion.value?.products.first - .map { .received($0) } ?? .failed + let payment = SKPayment(product: product) + interactor.addPayment(payment, for: accountData.number) - /// `@MainActor` isolation is safe here because - /// `ProductsRequestOperation` sets its `completionQueue` to `.main` - MainActor.assumeIsolated { - self?.setProductState(productState, animated: true) - } - } + setPaymentState(.makingPayment(payment), animated: true) } @MainActor @@ -176,8 +169,8 @@ class AccountViewController: UIViewController, @unchecked Sendable { applyViewState(animated: animated) } - private func setProductState(_ newState: ProductState, animated: Bool) { - productState = newState + private func setIsFetchingProducts(_ isFetchingProducts: Bool, animated: Bool = false) { + self.isFetchingProducts = isFetchingProducts applyViewState(animated: animated) } @@ -197,16 +190,16 @@ class AccountViewController: UIViewController, @unchecked Sendable { let purchaseButton = contentView.purchaseButton let activityIndicator = contentView.accountExpiryRowView.activityIndicator - if productState.isFetching || paymentState != .none { + if isFetchingProducts || paymentState != .none { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } - purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) - contentView.purchaseButton.isLoading = productState.isFetching +// purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) + contentView.purchaseButton.isLoading = isFetchingProducts - purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled + purchaseButton.isEnabled = !isFetchingProducts && isInteractionEnabled contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled) contentView.accountTokenRowView.setButtons(enabled: isInteractionEnabled) contentView.restorePurchasesView.setButtons(enabled: isInteractionEnabled) @@ -268,18 +261,30 @@ class AccountViewController: UIViewController, @unchecked Sendable { @objc private func deleteAccount() { actionHandler?(.navigateToDeleteAccount) } - - @objc private func doPurchase() { - guard case let .received(product) = productState, - let accountData = interactor.deviceState.accountData - else { + + @objc private func requestStoreProducts() { + guard let accountData = interactor.deviceState.accountData else { return } - - let payment = SKPayment(product: product) - interactor.addPayment(payment, for: accountData.number) - - setPaymentState(.makingPayment(payment), animated: true) + let productIdentifiers = Set(StoreSubscription.allCases) + setIsFetchingProducts(true) + _ = 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 { + actionHandler?(.showPurchaseOptions(PurchaseOptionDetails(products: products, accountNumber: accountData.number, didRequestPurchase: self.doPurchase))) + } else { + actionHandler?(.showFailedToLoadProducts) + } + case .failure: + actionHandler?(.showFailedToLoadProducts) + } + MainActor.assumeIsolated { + setIsFetchingProducts(false) + } + } } @objc private func restorePurchases() { diff --git a/ios/MullvadVPN/View controllers/Account/ProductState.swift b/ios/MullvadVPN/View controllers/Account/ProductState.swift deleted file mode 100644 index 8188a9b3c8..0000000000 --- a/ios/MullvadVPN/View controllers/Account/ProductState.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// ProductState.swift -// MullvadVPN -// -// Created by pronebird on 26/10/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import StoreKit - -enum ProductState { - case none - case fetching(StoreSubscription) - case received(SKProduct) - case failed - case cannotMakePurchases - - var isFetching: Bool { - if case .fetching = self { - return true - } - return false - } - - var isReceived: Bool { - if case .received = self { - return true - } - return false - } - - var purchaseButtonTitle: String? { - switch self { - case .none: - return nil - - case let .fetching(subscription): - return subscription.localizedTitle - - case let .received(product): - let localizedTitle = product.customLocalizedTitle ?? "" - let localizedPrice = product.localizedPrice ?? "" - - let format = NSLocalizedString( - "PURCHASE_BUTTON_TITLE_FORMAT", - tableName: "Account", - value: "%1$@ (%2$@)", - comment: "" - ) - return String(format: format, localizedTitle, localizedPrice) - - case .failed: - return NSLocalizedString( - "PURCHASE_BUTTON_CANNOT_CONNECT_TO_APPSTORE_LABEL", - tableName: "Account", - value: "Cannot connect to AppStore", - comment: "" - ) - - case .cannotMakePurchases: - return NSLocalizedString( - "PURCHASE_BUTTON_PAYMENTS_RESTRICTED_LABEL", - tableName: "Account", - value: "Payments restricted", - comment: "" - ) - } - } -} diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift index 17c9de2d2b..e338295cff 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift @@ -112,7 +112,7 @@ final class WelcomeContentView: UIView, Sendable { let localizedString = NSLocalizedString( "BUY_CREDIT_BUTTON", tableName: "Welcome", - value: "Buy credit", + value: "Add time", comment: "" ) button.setTitle(localizedString, for: .normal) @@ -182,11 +182,10 @@ final class WelcomeContentView: UIView, Sendable { } } - var productState: ProductState = .none { + var isFetchingProducts = false { didSet { - purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) - purchaseButton.isLoading = productState.isFetching - purchaseButton.isEnabled = productState.isReceived + purchaseButton.isLoading = isFetchingProducts + purchaseButton.isEnabled = !isFetchingProducts } } diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift index 11bddcf5aa..a052e812a5 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift @@ -21,15 +21,15 @@ final class WelcomeInteractor: @unchecked Sendable { private let logger = Logger(label: "\(WelcomeInteractor.self)") private var tunnelObserver: TunnelObserver? - private(set) var product: SKProduct? + private(set) var products: [SKProduct]? - var didChangeInAppPurchaseState: ((ProductState) -> Void)? var didAddMoreCredit: (() -> Void)? var viewDidLoad = false { didSet { guard viewDidLoad else { return } - requestAccessToStore() +// Might trigger a popup without user interaction do we want that? +// requestAccessToStore() } } @@ -77,20 +77,14 @@ final class WelcomeInteractor: @unchecked Sendable { self.tunnelObserver = tunnelObserver } - private func requestAccessToStore() { - if !StorePaymentManager.canMakePayments { - didChangeInAppPurchaseState?(.cannotMakePurchases) - } else { - let product = StoreSubscription.thirtyDays - didChangeInAppPurchaseState?(.fetching(product)) - _ = storePaymentManager.requestProducts(with: [product]) { [weak self] result in - guard let self else { return } - let product = result.value?.products.first - let productState: ProductState = product.map { .received($0) } ?? .failed - didChangeInAppPurchaseState?(productState) - self.product = product - } - } + func requestProducts( + with productIdentifiers: Set<StoreSubscription>, + completionHandler: @escaping (Result<SKProductsResponse, Error>) -> Void + ) -> Cancellable { + storePaymentManager.requestProducts( + with: productIdentifiers, + completionHandler: completionHandler + ) } private func startAccountUpdateTimer() { diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift index 909c22b824..00c9713fd1 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift @@ -12,7 +12,9 @@ import UIKit protocol WelcomeViewControllerDelegate: AnyObject { func didRequestToRedeemVoucher(controller: WelcomeViewController) func didRequestToShowInfo(controller: WelcomeViewController) - func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) +// func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) + func didRequestToViewPurchaseOptions(controller: WelcomeViewController, availableProducts: [SKProduct], accountNumber: String) + func didRequestToShowFailToFetchProducts(controller: WelcomeViewController) } class WelcomeViewController: UIViewController, RootContainment { @@ -59,10 +61,17 @@ class WelcomeViewController: UIViewController, RootContainment { super.viewDidLoad() configureUI() contentView.viewModel = interactor.viewModel - interactor.didChangeInAppPurchaseState = { [weak self] productState in - guard let self else { return } - self.contentView.productState = productState - } +// 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 } @@ -89,12 +98,22 @@ extension WelcomeViewController: @preconcurrency WelcomeContentViewDelegate { } func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) { - interactor.product.flatMap { - delegate?.didRequestToPurchaseCredit( - controller: self, - accountNumber: interactor.accountNumber, - product: $0 - ) + let productIdentifiers = Set(StoreSubscription.allCases) + contentView.isFetchingProducts = true + _ = 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?.didRequestToViewPurchaseOptions(controller: self, availableProducts: products, accountNumber: interactor.accountNumber) + } else { + delegate?.didRequestToShowFailToFetchProducts(controller: self) + } + case .failure: + delegate?.didRequestToShowFailToFetchProducts(controller: self) + } + contentView.isFetchingProducts = false } } diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift index 5f26cd4643..02dee6b9b5 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift @@ -24,12 +24,6 @@ class OutOfTimeViewController: UIViewController, RootContainment { private let interactor: OutOfTimeInteractor private let errorPresenter: PaymentAlertPresenter - private var productState: ProductState = .none { - didSet { - applyViewState() - } - } - private var paymentState: PaymentState = .none { didSet { applyViewState() @@ -84,11 +78,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(doPurchase), +// for: .touchUpInside +// ) contentView.restoreButton.addTarget( self, action: #selector(restorePurchases), @@ -111,7 +105,8 @@ class OutOfTimeViewController: UIViewController, RootContainment { if StorePaymentManager.canMakePayments { requestStoreProducts() } else { - productState = .cannotMakePurchases + // Show popup +// productState = .cannotMakePurchases } } @@ -128,18 +123,16 @@ class OutOfTimeViewController: UIViewController, RootContainment { // MARK: - Private private func requestStoreProducts() { - let productKind = StoreSubscription.thirtyDays - - productState = .fetching(productKind) - - _ = interactor.requestProducts(with: [productKind]) { [weak self] completion in - let productState: ProductState = completion.value?.products.first - .map { .received($0) } ?? .failed - - Task { @MainActor in - self?.productState = productState - } - } +// 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() { @@ -149,11 +142,12 @@ class OutOfTimeViewController: UIViewController, RootContainment { let isOutOfTime = interactor.deviceState.accountData.map { $0.expiry < Date() } ?? false - purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) - contentView.purchaseButton.isLoading = productState.isFetching +// purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal) + // do this at the appropriate position +// contentView.purchaseButton.isLoading = productState.isFetching - purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled && !tunnelState - .isSecured +// purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled && !tunnelState +// .isSecured contentView.restoreButton.isEnabled = isInteractionEnabled contentView.enableDisconnectButton(tunnelState.isSecured, animated: true) @@ -231,18 +225,18 @@ class OutOfTimeViewController: UIViewController, RootContainment { // 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 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 func restorePurchases() { guard let accountData = interactor.deviceState.accountData else { |
