diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-04-02 15:37:26 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-04-02 15:37:26 +0200 |
| commit | a3bfa6581e70164fd86df25332132c38a92fef20 (patch) | |
| tree | 7e082ca4779b78606ab75defa042e5887ff8564e | |
| parent | 9c5d0089dd5f052d6b76ad9dabea40a97f624f96 (diff) | |
| parent | 17a3ea0f079f9439ae3f9fd63bc2df2362aa8d65 (diff) | |
| download | mullvadvpn-a3bfa6581e70164fd86df25332132c38a92fef20.tar.xz mullvadvpn-a3bfa6581e70164fd86df25332132c38a92fef20.zip | |
Merge branch 'handle-iap-fetch-failure'
| -rw-r--r-- | ios/MullvadVPN/Account.swift | 56 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountExpiry.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountViewController.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppStorePaymentManager.swift | 28 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.swift | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadAPI.swift | 47 |
7 files changed, 88 insertions, 98 deletions
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift index 065a04d111..942b8c5a39 100644 --- a/ios/MullvadVPN/Account.swift +++ b/ios/MullvadVPN/Account.swift @@ -27,11 +27,14 @@ enum AccountError: Error { /// A enum describing the error emitted during login enum AccountLoginError: Error { case invalidAccount + case network(MullvadAPI.Error) + case communication(MullvadAPI.ResponseError) case tunnelConfiguration(TunnelManagerError) } enum CreateAccountError: Error { - case newAccountToken + case network(MullvadAPI.Error) + case communication(MullvadAPI.ResponseError) case tunnelConfiguration(TunnelManagerError) } @@ -51,12 +54,15 @@ extension AccountError: LocalizedError { var failureReason: String? { switch self { - case .createNew(.newAccountToken): + case .createNew(.network), .createNew(.communication): return NSLocalizedString("Failed to create new account", comment: "") case .login(.invalidAccount): return NSLocalizedString("Invalid account", comment: "") + case .login(.network), .login(.communication): + return NSLocalizedString("Network error", comment: "") + case .login(.tunnelConfiguration(.setAccount(let setAccountError))), .createNew(.tunnelConfiguration(.setAccount(let setAccountError))): switch setAccountError { @@ -132,10 +138,10 @@ class Account { func loginWithNewAccount() -> AnyPublisher<String, AccountError> { return apiClient.createAccount() - .mapError { _ in CreateAccountError.newAccountToken } + .mapError { CreateAccountError.network($0) } .flatMap { (response) -> AnyPublisher<(String, Date), CreateAccountError> in response.result - .mapError { _ in CreateAccountError.newAccountToken } + .mapError { CreateAccountError.communication($0) } .publisher .flatMap { (accountToken) in TunnelManager.shared.setAccount(accountToken: accountToken) @@ -154,23 +160,18 @@ class Account { /// Perform the login and save the account token along with expiry (if available) to the /// application preferences. func login(with accountToken: String) -> AnyPublisher<(), AccountError> { - return apiClient.verifyAccount(accountToken: accountToken) - .setFailureType(to: AccountLoginError.self) - .handleEvents(receiveOutput: { (accountVerification) in - if case .deferred(let error) = accountVerification { - os_log(.error, "Failed to verify the account: %{public}s", error.localizedDescription) - } - }) - .flatMap { - self.handleVerification($0).publisher - .flatMap { (expiry) in - TunnelManager.shared.setAccount(accountToken: accountToken) - .mapError { AccountLoginError.tunnelConfiguration($0) } - .map { expiry } - } - }.mapError { AccountError.login($0) } - .receive(on: DispatchQueue.main).map { (expiry) in - self.saveAccountToPreferences(accountToken: accountToken, expiry: expiry) + return apiClient.getAccountExpiry(accountToken: accountToken) + .mapError { AccountLoginError.network($0) } + .flatMap { (response) in + response.result + .mapError { AccountLoginError.communication($0) } + .publisher + }.flatMap { (expiry) in + TunnelManager.shared.setAccount(accountToken: accountToken) + .mapError { AccountLoginError.tunnelConfiguration($0) } + .map { expiry } + }.mapError { AccountError.login($0) }.receive(on: DispatchQueue.main).map { (expiry) in + self.saveAccountToPreferences(accountToken: accountToken, expiry: expiry) }.eraseToAnyPublisher() } @@ -183,18 +184,7 @@ class Account { .eraseToAnyPublisher() } - private func handleVerification(_ verification: AccountVerification) -> Result<Date?, AccountLoginError> { - switch verification { - case .deferred: - return .success(nil) - case .verified(let expiry): - return .success(expiry) - case .invalid: - return .failure(.invalidAccount) - } - } - - private func saveAccountToPreferences(accountToken: String, expiry: Date?) { + private func saveAccountToPreferences(accountToken: String, expiry: Date) { let preferences = UserDefaults.standard preferences.set(accountToken, forKey: UserDefaultsKeys.accountToken.rawValue) diff --git a/ios/MullvadVPN/AccountExpiry.swift b/ios/MullvadVPN/AccountExpiry.swift index 85f3159d62..87d40e87fc 100644 --- a/ios/MullvadVPN/AccountExpiry.swift +++ b/ios/MullvadVPN/AccountExpiry.swift @@ -25,7 +25,7 @@ class AccountExpiry { } var isExpired: Bool { - return date < Date() + return date <= Date() } var formattedRemainingTime: String? { diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index c9fd33dff3..d49e156532 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -30,7 +30,10 @@ class AccountViewController: UIViewController { private lazy var purchaseButtonInteractionRestriction = UserInterfaceInteractionRestriction(scheduler: DispatchQueue.main) { [weak self] (enableUserInteraction, _) in - self?.purchaseButton.isEnabled = enableUserInteraction + // Make sure to disable the button if the product is not loaded + self?.purchaseButton.isEnabled = enableUserInteraction && + self?.product != nil && + AppStorePaymentManager.canMakePayments } private lazy var viewControllerInteractionRestriction = @@ -72,7 +75,12 @@ class AccountViewController: UIViewController { updateAccountExpiry(expiryDate: expiryDate) } - requestStoreProducts() + // Make sure to disable IAPs when payments are restricted + if AppStorePaymentManager.canMakePayments { + requestStoreProducts() + } else { + setPaymentsRestricted() + } } // MARK: - Private methods @@ -93,13 +101,15 @@ class AccountViewController: UIViewController { purchaseButton.isLoading = true requestProductsSubscriber = AppStorePaymentManager.shared.requestProducts(with: [.thirtyDays]) - .retry(1) + .retry(10) .receive(on: DispatchQueue.main) .restrictUserInterfaceInteraction(with: self.purchaseButtonInteractionRestriction, animated: true) .sink(receiveCompletion: { [weak self] (completion) in - if case .finished = completion { - self?.purchaseButton.isLoading = false + if case .failure(let error) = completion { + self?.didFailLoadingProducts(with: error) } + + self?.purchaseButton.isLoading = false }, receiveValue: { [weak self] (response) in if let product = response.products.first { self?.setProduct(product, animated: true) @@ -110,10 +120,32 @@ class AccountViewController: UIViewController { private func setProduct(_ product: SKProduct, animated: Bool) { self.product = product + let localizedTitle = product.customLocalizedTitle ?? "" let localizedPrice = product.localizedPrice ?? "" - let title = String(format: NSLocalizedString("%@ (%@)", comment: ""), - product.localizedTitle, localizedPrice) + + let format = NSLocalizedString( + "%1$@ (%2$@)", + comment: "The buy button title: <TITLE> (<PRICE>). The order can be changed by swapping %1 and %2." + ) + let title = String(format: format, localizedTitle, localizedPrice) + + purchaseButton.setTitle(title, for: .normal) + } + + private func didFailLoadingProducts(with error: Error) { + let title = NSLocalizedString( + "Cannot connect to AppStore", + comment: "The buy button title displayed when unable to load the price of subscription" + ) + + purchaseButton.setTitle(title, for: .normal) + } + + private func setPaymentsRestricted() { + let title = NSLocalizedString("Payments restricted", comment: "") + purchaseButton.setTitle(title, for: .normal) + purchaseButton.isEnabled = false } private func setEnableUserInteraction(_ enableUserInteraction: Bool, animated: Bool) { diff --git a/ios/MullvadVPN/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager.swift index 7fada5ca67..04a4928fe4 100644 --- a/ios/MullvadVPN/AppStorePaymentManager.swift +++ b/ios/MullvadVPN/AppStorePaymentManager.swift @@ -11,12 +11,25 @@ import Foundation import StoreKit import os -enum InAppPurchase: String { - /// Thirty days worth of credit - case thirtyDays = "net.mullvad.MullvadVPN.iap.30days" +enum AppStoreSubscription: String { + /// Thirty days non-renewable subscription + case thirtyDays = "net.mullvad.MullvadVPN.subscription.30days" + + var localizedTitle: String { + switch self { + case .thirtyDays: + return NSLocalizedString("Add 30 days time", comment: "") + } + } } -extension Set where Element == InAppPurchase { +extension SKProduct { + var customLocalizedTitle: String? { + return AppStoreSubscription(rawValue: productIdentifier)?.localizedTitle + } +} + +extension Set where Element == AppStoreSubscription { var productIdentifiersSet: Set<String> { Set<String>(self.map { $0.rawValue }) } @@ -111,6 +124,11 @@ class AppStorePaymentManager { /// A private hash map that maps each payment to account token private var paymentToAccountToken = [SKPayment: String]() + /// Returns true if the device is able to make payments + class var canMakePayments: Bool { + return SKPaymentQueue.canMakePayments() + } + init(queue: SKPaymentQueue) { self.queue = queue } @@ -179,7 +197,7 @@ class AppStorePaymentManager { // MARK: - Products and payments - func requestProducts(with productIdentifiers: Set<InAppPurchase>) + func requestProducts(with productIdentifiers: Set<AppStoreSubscription>) -> SKRequestPublisher<SKProductsRequestSubscription> { let productIdentifiers = productIdentifiers.productIdentifiersSet diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 65defd360f..bef9afe4ed 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -154,12 +154,8 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie showedAccountView = true - switch Account.shared.expiry?.compare(Date()) { - case .orderedAscending, .orderedSame: + if let accountExpiry = Account.shared.expiry, AccountExpiry(date: accountExpiry).isExpired { rootContainerController?.showSettings(navigateTo: .account, animated: true) - - default: - break } } diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index d7d7707e10..393e455bb0 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -169,6 +169,7 @@ class LoginViewController: UIViewController, UITextFieldDelegate, RootContainmen beginLogin(method: .newAccount) accountTextField.text = "" + updateKeyboardToolbar() loginSubscriber = Account.shared.loginWithNewAccount() .receive(on: DispatchQueue.main) diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift index 4fbaa1d430..95a01a27a7 100644 --- a/ios/MullvadVPN/MullvadAPI.swift +++ b/ios/MullvadVPN/MullvadAPI.swift @@ -16,28 +16,6 @@ private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")! /// Network request timeout in seconds private let kNetworkTimeout: TimeInterval = 10 -/// A type that describes the account verification result -enum AccountVerification { - /// The app should attempt to verify the account token at some point later because the network - /// may not be available at this time. - case deferred(DeferReasonError) - - /// The app successfully verified the account token with the server - case verified(Date) - - // Invalid token - case invalid -} - -/// An error type that describes why the account verification was deferred -enum DeferReasonError: Error { - /// Mullvad API communication error - case communication(MullvadAPI.Error) - - /// Mullvad API responded with an error - case server(MullvadAPI.ResponseError) -} - /// A response received when sending the AppStore receipt to the backend struct SendAppStoreReceiptResponse: Codable { let timeAdded: TimeInterval @@ -132,31 +110,6 @@ class MullvadAPI { return MullvadAPI.makeDataTaskPublisher(request: request) } - func verifyAccount(accountToken: String) -> AnyPublisher<AccountVerification, Never> { - return getAccountExpiry(accountToken: accountToken) - .map({ (response) -> AccountVerification in - switch response.result { - case .success(let expiry): - // Report .verified when expiry is successfully received - return .verified(expiry) - - case .failure(let serverError): - if case .accountDoesNotExist = serverError.code { - // Report .invalid account if the server responds with the special code - return .invalid - } else { - // Otherwise report .deferred and pass the server error along - return .deferred(.server(serverError)) - } - } - }) - .catch({ (networkError) in - // Treat all communication errors as .deferred verification - return Just(.deferred(.communication(networkError))) - }) - .eraseToAnyPublisher() - } - func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Response<WireguardAssociatedAddresses>, MullvadAPI.Error> { let request = JsonRpcRequest(method: "push_wg_key", params: [ AnyEncodable(accountToken), |
