summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-04-02 15:37:26 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-04-02 15:37:26 +0200
commita3bfa6581e70164fd86df25332132c38a92fef20 (patch)
tree7e082ca4779b78606ab75defa042e5887ff8564e
parent9c5d0089dd5f052d6b76ad9dabea40a97f624f96 (diff)
parent17a3ea0f079f9439ae3f9fd63bc2df2362aa8d65 (diff)
downloadmullvadvpn-a3bfa6581e70164fd86df25332132c38a92fef20.tar.xz
mullvadvpn-a3bfa6581e70164fd86df25332132c38a92fef20.zip
Merge branch 'handle-iap-fetch-failure'
-rw-r--r--ios/MullvadVPN/Account.swift56
-rw-r--r--ios/MullvadVPN/AccountExpiry.swift2
-rw-r--r--ios/MullvadVPN/AccountViewController.swift46
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager.swift28
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift6
-rw-r--r--ios/MullvadVPN/LoginViewController.swift1
-rw-r--r--ios/MullvadVPN/MullvadAPI.swift47
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),