summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorSteffen Ernst <steffen@Steffens-MacBook-Pro.local>2025-01-13 10:41:56 +0100
committerSteffen Ernst <steffen.ernst@mullvad.net>2025-01-20 15:54:11 +0100
commit426cc4241a91539361bdabd08958a5e04788dbf1 (patch)
tree7506dde59ad557e06b6ef135fc13e99738e66e93
parentabefbd407a6191a044fea4cc58e431118587fcd9 (diff)
downloadmullvadvpn-426cc4241a91539361bdabd08958a5e04788dbf1.tar.xz
mullvadvpn-426cc4241a91539361bdabd08958a5e04788dbf1.zip
Add 90 day payment to welcome and account view
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/AccountCoordinator.swift69
-rw-r--r--ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift64
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift18
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift6
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift87
-rw-r--r--ios/MullvadVPN/View controllers/Account/ProductState.swift70
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift9
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift28
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift41
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift74
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 {