summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorSteffen Ernst <steffen@Steffens-MacBook-Pro.local>2025-01-13 14:01:24 +0100
committerSteffen Ernst <steffen.ernst@mullvad.net>2025-01-20 15:54:12 +0100
commit7261493c00339a9945d4ac32161e8a245b975b6e (patch)
tree3e90d636d06bfbe589df403f71d3eca22d75e843
parent9ce9f045d239d2863f1c7b75d4e9434cf1de8e8e (diff)
downloadmullvadvpn-7261493c00339a9945d4ac32161e8a245b975b6e.tar.xz
mullvadvpn-7261493c00339a9945d4ac32161e8a245b975b6e.zip
Add 90 day payment option to our of time view
-rw-r--r--ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift64
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift12
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift94
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 {