summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-12-05 13:13:51 +0100
committerAndrej Mihajlov <and@mullvad.net>2019-12-05 16:05:10 +0100
commita418befe7f428ac260897cf800f0d042f8e010f9 (patch)
tree1d9846c065f1ce409fd728bc538558633119036b /ios
parent61f047106d0dc82f9233c46023388f49780e0400 (diff)
downloadmullvadvpn-a418befe7f428ac260897cf800f0d042f8e010f9.tar.xz
mullvadvpn-a418befe7f428ac260897cf800f0d042f8e010f9.zip
Wire up Account with TunnelManager
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN/Account.swift116
-rw-r--r--ios/MullvadVPN/AccountExpiryRefresh.swift135
-rw-r--r--ios/MullvadVPN/AccountVerificationProcedure.swift88
-rw-r--r--ios/MullvadVPN/AccountViewController.swift23
-rw-r--r--ios/MullvadVPN/UserDefaultsInteractor.swift54
5 files changed, 87 insertions, 329 deletions
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 2c006dd8c5..0d98134769 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -6,67 +6,107 @@
// Copyright © 2019 Amagicom AB. All rights reserved.
//
+import Combine
import Foundation
-import ProcedureKit
-import os.log
+import os
+
+/// A enum describing the errors emitted by `Account`
+enum AccountError: Error {
+ /// A failure to perform the login
+ case login(AccountLoginError)
+
+ /// A failure to log out
+ case logout(TunnelManagerError)
+}
+
+/// A enum describing the error emitted during login
+enum AccountLoginError: Error {
+ case invalidAccount
+ case tunnelConfiguration(TunnelManagerError)
+}
+
+/// A enum holding the `UserDefaults` string keys
+private enum UserDefaultsKeys: String {
+ case accountToken = "accountToken"
+ case accountExpiry = "accountExpiry"
+}
/// A class that groups the account related operations
class Account {
- enum Error: Swift.Error {
- case invalidAccount
- }
+ static let shared = Account()
+ private let apiClient = MullvadAPI()
/// Returns the currently used account token
- static var token: String? {
- return UserDefaultsInteractor.sharedApplicationGroupInteractor.accountToken
+ var token: String? {
+ return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue)
}
/// Returns the account expiry for the currently used account token
- static var expiry: Date? {
- return UserDefaultsInteractor.sharedApplicationGroupInteractor.accountExpiry
+ var expiry: Date? {
+ return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date
}
- static var isLoggedIn: Bool {
+ var isLoggedIn: Bool {
return token != nil
}
/// Perform the login and save the account token along with expiry (if available) to the
/// application preferences.
- class func login(with accountToken: String) -> Procedure {
- let userDefaultsInteractor = UserDefaultsInteractor.sharedApplicationGroupInteractor
-
- // Request account token verification
- let verificationProcedure = AccountVerificationProcedure(accountToken: accountToken)
-
- // Update the application preferences based on the AccountVerification result.
- let saveAccountDataProcedure = TransformProcedure { (verification) in
- switch verification {
- case .verified(let expiry):
- userDefaultsInteractor.accountToken = accountToken
- userDefaultsInteractor.accountExpiry = expiry
+ 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)
+ }.eraseToAnyPublisher()
+ }
- case .deferred(let error):
- userDefaultsInteractor.accountToken = accountToken
- userDefaultsInteractor.accountExpiry = nil
+ /// Perform the logout by erasing the account token and expiry from the application preferences.
+ func logout() -> AnyPublisher<(), AccountError> {
+ return TunnelManager.shared.unsetAccount()
+ .receive(on: DispatchQueue.main)
+ .mapError { AccountError.logout($0) }
+ .map(self.removeAccountFromPreferences)
+ .eraseToAnyPublisher()
+ }
- os_log(.info, #"Could not request the account verification "%{private}s": %{public}s"#,
- accountToken, error.localizedDescription)
+ 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)
+ }
+ }
- case .invalid:
- throw Error.invalidAccount
- }
- }.injectResult(from: verificationProcedure)
+ private func saveAccountToPreferences(accountToken: String, expiry: Date?) {
+ let preferences = UserDefaults.standard
- return GroupProcedure(operations: [verificationProcedure, saveAccountDataProcedure])
+ preferences.set(accountToken, forKey: UserDefaultsKeys.accountToken.rawValue)
+ preferences.set(expiry, forKey: UserDefaultsKeys.accountExpiry.rawValue)
}
- /// Perform the logout by erasing the account token and expiry from the application preferences.
- class func logout() {
- let userDefaultsInteractor = UserDefaultsInteractor.sharedApplicationGroupInteractor
+ private func removeAccountFromPreferences() {
+ let preferences = UserDefaults.standard
- userDefaultsInteractor.accountToken = nil
- userDefaultsInteractor.accountExpiry = nil
- }
+ preferences.removeObject(forKey: UserDefaultsKeys.accountToken.rawValue)
+ preferences.removeObject(forKey: UserDefaultsKeys.accountExpiry.rawValue)
+ }
}
+
diff --git a/ios/MullvadVPN/AccountExpiryRefresh.swift b/ios/MullvadVPN/AccountExpiryRefresh.swift
deleted file mode 100644
index c6b1c6278b..0000000000
--- a/ios/MullvadVPN/AccountExpiryRefresh.swift
+++ /dev/null
@@ -1,135 +0,0 @@
-//
-// AccountExpiryRefresh.swift
-// MullvadVPN
-//
-// Created by pronebird on 28/05/2019.
-// Copyright © 2019 Amagicom AB. All rights reserved.
-//
-
-import Foundation
-import os.log
-import ProcedureKit
-
-private let kRefreshIntervalSeconds: TimeInterval = 60
-
-/// A class that manages the periodic account expiry updates.
-/// All public methods are thread safe.
-class AccountExpiryRefresh {
-
- /// A singleton instance of the AccountExpiryRefresh
- static let shared = AccountExpiryRefresh()
-
- private let procedureQueue: ProcedureQueue = {
- let queue = ProcedureQueue()
- queue.qualityOfService = .utility
- return queue
- }()
-
- /// Recursive lock used to manipulate observers
- private let lock = NSRecursiveLock()
- private var observers = [WeakBox<Observer>]()
-
- private init() {}
-
- /// Adds the observer for periodic account expiry updates.
- func startMonitoringUpdates(with block: @escaping (Date) -> Void) -> Observer {
- let observer = Observer(with: block)
-
- addObserver(observer)
-
- return observer
- }
-
- /// Register observer and start updating the account expiry if hasn't started yet
- private func addObserver(_ observer: Observer) {
- lock.withCriticalScope {
- let wasEmpty = observers.isEmpty
-
- observers.append(WeakBox(observer))
-
- if wasEmpty {
- procedureQueue.addOperation(makePeriodicUpdateProcedure())
- }
- }
-
- }
-
- /// Remove all boxed values whos underlying weak value has been released
- private func compactObservers() {
- lock.withCriticalScope {
- observers.removeAll { $0.unboxed == nil }
- }
- }
-
- /// Broadcast the new expiry to the observers
- private func notifyObservers(with newExpiry: Date) {
- let strongObservers = lock.withCriticalScope { observers.compactMap { $0.unboxed } }
-
- DispatchQueue.main.async {
- strongObservers.forEach { $0.notify(with: newExpiry) }
- }
- }
-
- /// Returns true if the repeat procedure should keep running
- private func shouldKeepRefreshing() -> Bool {
- return lock.withCriticalScope {
- compactObservers()
-
- return !observers.isEmpty
- }
- }
-
- /// Create a procedure that will repeat itself with a constant interval until there are no
- /// observers left.
- private func makePeriodicUpdateProcedure() -> RepeatProcedure<Operation> {
- let repeatProcedure = RepeatProcedure(wait: .constant(kRefreshIntervalSeconds)) { [weak self] () -> Operation? in
- // Stop repeating the procedure if no-one is listening
- guard let self = self, self.shouldKeepRefreshing() else { return nil }
-
- // Create the procedure to feed the account token saved in preferences into the
- // request procedure
- let getAccountTokenProcedure = ResultProcedure(block: { Account.token })
-
- // Create the API request procedure
- let requestProcedure = MullvadAPI.getAccountExpiry()
- .injectResult(from: getAccountTokenProcedure)
-
- // Create the procedure to save the received account expiry and notify the observers
- let saveAccountExpiryProcedure = TransformProcedure { [weak self] (response) throws -> Void in
- let userDefaultsInteractor = UserDefaultsInteractor.sharedApplicationGroupInteractor
-
- let newAccountExpiry = try response.result.get()
- let oldAccountExpiry = userDefaultsInteractor.accountExpiry
-
- if oldAccountExpiry != newAccountExpiry {
- userDefaultsInteractor.accountExpiry = newAccountExpiry
-
- self?.notifyObservers(with: newAccountExpiry)
- }
- }.injectResult(from: requestProcedure)
-
- // Return the group
- return GroupProcedure(operations: [getAccountTokenProcedure, requestProcedure, saveAccountExpiryProcedure])
- }
-
- // Make sure that only one such operation runs at a time
- repeatProcedure.addCondition(MutuallyExclusive<AccountExpiryRefresh>())
-
- return repeatProcedure
- }
-
- /// The account expiry observer.
- class Observer {
- typealias Block = (Date) -> Void
- private let block: Block
-
- fileprivate init(with block: @escaping Block) {
- self.block = block
- }
-
- fileprivate func notify(with expiryDate: Date) {
- block(expiryDate)
- }
- }
-
-}
diff --git a/ios/MullvadVPN/AccountVerificationProcedure.swift b/ios/MullvadVPN/AccountVerificationProcedure.swift
deleted file mode 100644
index f3b31062b5..0000000000
--- a/ios/MullvadVPN/AccountVerificationProcedure.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-//
-// AccountVerificationProcedure.swift
-// MullvadVPN
-//
-// Created by pronebird on 14/05/2019.
-// Copyright © 2019 Amagicom AB. All rights reserved.
-//
-
-import Foundation
-import ProcedureKit
-
-/// 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(Error)
-
- /// The app successfully verified the account token with the server
- case verified(Date)
-
- // Invalid token
- case invalid
-}
-
-/// The error code returned by the API when it cannot find the given account token
-private let kAccountDoesNotExistErrorCode = -200
-
-/// The procedure that implements account verification by sending the account expiry request to the
-/// Mullvad API. This procedure is non-fallable so even in the case of network issues it will set
-/// the output and return no errors.
-class AccountVerificationProcedure: GroupProcedure, InputProcedure, OutputProcedure {
- var input: Pending<String>
- var output: Pending<ProcedureResult<AccountVerification>> = .pending
-
- init(dispatchQueue underlyingQueue: DispatchQueue? = nil, accountToken: String? = nil) {
- self.input = accountToken.flatMap { .ready($0) } ?? .pending
-
- // Request account data from the API
- let networkRequest = MullvadAPI.getAccountExpiry(accountToken: accountToken)
-
- super.init(dispatchQueue: underlyingQueue, operations: [
- // Wrap the network request into the ignoreErrorsProcedure to make sure that any network
- // or JSON decoding errors do not get propagates. These errors will be returned along
- // with the AccountVerification via the output.
- IgnoreErrorsProcedure(dispatchQueue: underlyingQueue, operation: networkRequest)
- ])
-
- // Copy the input of the group procedure to the input of the starting procedure
- addWillExecuteBlockObserver { [weak networkRequest] (groupProcedure, _) in
- networkRequest?.input = groupProcedure.input
- }
-
- networkRequest.addWillFinishBlockObserver { [weak self] (networkRequest, error, _) in
- guard let self = self else { return }
-
- // Obtain the network error or the procedure result
- guard let procedureResult = error.flatMap({ .failure($0) })
- ?? networkRequest.output.value else { return }
-
- // Do not set the output if the network request was cancelled
- if !networkRequest.isCancelled {
- self.output = .ready(.success(self.mapResult(procedureResult)))
- }
- }
- }
-
- private func mapResult(_ procedureResult: ProcedureResult<JsonRpcResponse<Date>>) -> Output {
- // Unwrap the result of the network request procedure
- switch procedureResult {
- case .success(let response):
- // Unwrap the JSON RPC response
- switch response.result {
- case .success(let expiryDate):
- return .verified(expiryDate)
-
- case .failure(let serverError):
- if serverError.code == kAccountDoesNotExistErrorCode {
- return .invalid
- } else {
- return .deferred(serverError)
- }
- }
- case .failure(let networkError):
- // Check back later in case of network issues
- return .deferred(networkError)
- }
- }
-}
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index a52cee7e83..c51a8e6977 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -6,6 +6,7 @@
// Copyright © 2019 Amagicom AB. All rights reserved.
//
+import Combine
import UIKit
class AccountViewController: UIViewController {
@@ -13,13 +14,12 @@ class AccountViewController: UIViewController {
@IBOutlet var accountLabel: UILabel!
@IBOutlet var expiryLabel: UILabel!
- private var accountExpiryObserver: AccountExpiryRefresh.Observer?
+ private var logoutSubscriber: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
updateView()
- startAccountExpiryUpdates()
}
// MARK: - Actions
@@ -29,17 +29,19 @@ class AccountViewController: UIViewController {
}
@IBAction func doLogout() {
- Account.logout()
-
- performSegue(withIdentifier: SegueIdentifier.Account.logout.rawValue, sender: self)
+ logoutSubscriber = Account.shared.logout()
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { (_) in
+ self.performSegue(withIdentifier: SegueIdentifier.Account.logout.rawValue, sender: self)
+ })
}
// MARK: - Private
private func updateView() {
- accountLabel.text = Account.token
+ accountLabel.text = Account.shared.token
- if let expiryDate = Account.expiry {
+ if let expiryDate = Account.shared.expiry {
let accountExpiry = AccountExpiry(date: expiryDate)
if accountExpiry.isExpired {
@@ -51,11 +53,4 @@ class AccountViewController: UIViewController {
}
}
}
-
- private func startAccountExpiryUpdates() {
- accountExpiryObserver = AccountExpiryRefresh.shared
- .startMonitoringUpdates(with: { [weak self] (expiryDate) in
- self?.updateView()
- })
- }
}
diff --git a/ios/MullvadVPN/UserDefaultsInteractor.swift b/ios/MullvadVPN/UserDefaultsInteractor.swift
deleted file mode 100644
index 786611e448..0000000000
--- a/ios/MullvadVPN/UserDefaultsInteractor.swift
+++ /dev/null
@@ -1,54 +0,0 @@
-//
-// UserDefaultsInteractor.swift
-// MullvadVPN
-//
-// Created by pronebird on 15/05/2019.
-// Copyright © 2019 Amagicom AB. All rights reserved.
-//
-
-import Foundation
-
-/// The application group identifier used for sharing application preferences between processes
-private let kApplicationGroupIdentifier = "group.net.mullvad.MullvadVPN"
-
-/// The UserDefaults keys used to store the application preferences
-private enum UserDefaultsKeys: String {
- case accountToken, accountExpiry
-}
-
-/// The interactor class that provides a convenient interface for accessing the Mullvad VPN
-/// preferences stored in the UserDefaults store.
-class UserDefaultsInteractor {
- let userDefaults: UserDefaults
-
- /// The shared instance of UserDefaultsInteractor initialized with the application group
- /// preferences
- static let sharedApplicationGroupInteractor: UserDefaultsInteractor = {
- let userDefaults = UserDefaults(suiteName: kApplicationGroupIdentifier)!
-
- return UserDefaultsInteractor(userDefaults: userDefaults)
- }()
-
- init(userDefaults: UserDefaults) {
- self.userDefaults = userDefaults
- }
-
- var accountToken: String? {
- get {
- return userDefaults.string(forKey: UserDefaultsKeys.accountToken.rawValue)
- }
- set {
- userDefaults.set(newValue, forKey: UserDefaultsKeys.accountToken.rawValue)
- }
- }
-
- var accountExpiry: Date? {
- get {
- return userDefaults.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date
- }
- set {
- userDefaults.set(newValue, forKey: UserDefaultsKeys.accountExpiry.rawValue)
- }
- }
-
-}