diff --git a/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift b/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift
index 4505c5c2..92059b75 100644
--- a/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift
+++ b/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift
@@ -32,8 +32,8 @@ public class AccountFactory {
SubscriptionsUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(), refreshAuthTokensChecker: makeRefreshAuthTokensChecker())
}
- static func makeDefaultAccountProvider(with webServices: WebServices? = nil) -> DefaultAccountProvider {
- DefaultAccountProvider(
+ static func makeDefaultAccountProvider(with webServices: WebServices? = nil) -> NativeDefaultAccountProvider {
+ NativeDefaultAccountProvider(
webServices: webServices,
logoutUseCase: makeLogoutUseCase(),
loginUseCase: makeLoginUseCase(),
@@ -47,7 +47,6 @@ public class AccountFactory {
deleteAccountUseCase: makeDeleteAccountUseCase(),
featureFlagsUseCase: makeFeatureFlagsUseCase()
)
-
}
static func makeRefreshAPITokenUseCase() -> RefreshAPITokenUseCaseType {
diff --git a/Sources/PIALibrary/Account/NativeDefaultAccountProvider.swift b/Sources/PIALibrary/Account/NativeDefaultAccountProvider.swift
new file mode 100644
index 00000000..ae9afd8a
--- /dev/null
+++ b/Sources/PIALibrary/Account/NativeDefaultAccountProvider.swift
@@ -0,0 +1,730 @@
+//
+// NativeDefaultAccountProvider.swift
+// PIALibrary
+//
+// Created by Davide De Rosa on 10/2/17.
+// Copyright © 2020 Private Internet Access, Inc.
+//
+// This file is part of the Private Internet Access iOS Client.
+//
+// The Private Internet Access iOS Client is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option) any later version.
+//
+// The Private Internet Access iOS Client is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with the Private
+// Internet Access iOS Client. If not, see .
+//
+
+import Foundation
+import SwiftyBeaver
+import UIKit
+
+private let log = SwiftyBeaver.self
+
+@available(tvOS 17.0, *)
+open class NativeDefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAccess, WebServicesAccess, InAppAccess, WebServicesConsumer {
+
+ private let customWebServices: WebServices?
+
+ private let logoutUseCase: LogoutUseCaseType
+ private let loginUseCase: LoginUseCaseType
+ private let signupUseCase: SignupUseCaseType
+ private let apiTokenProvider: APITokenProviderType
+ private let vpnTokenProvider: VpnTokenProviderType
+ private let accountDetailsUseCase: AccountDetailsUseCaseType
+ private let updateAccountUseCase: UpdateAccountUseCaseType
+ private let paymentUseCase: PaymentUseCaseType
+ private let subscriptionsUseCase: SubscriptionsUseCaseType
+ private let deleteAccountUseCase: DeleteAccountUseCaseType
+ private let featureFlagsUseCase: FeatureFlagsUseCaseType
+
+
+ init(webServices: WebServices? = nil, logoutUseCase: LogoutUseCaseType, loginUseCase: LoginUseCaseType, signupUseCase: SignupUseCaseType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType, accountDetailsUseCase: AccountDetailsUseCaseType, updateAccountUseCase: UpdateAccountUseCaseType, paymentUseCase: PaymentUseCaseType, subscriptionsUseCase: SubscriptionsUseCaseType, deleteAccountUseCase: DeleteAccountUseCaseType, featureFlagsUseCase: FeatureFlagsUseCaseType) {
+ self.logoutUseCase = logoutUseCase
+ self.loginUseCase = loginUseCase
+ self.signupUseCase = signupUseCase
+ self.apiTokenProvider = apiTokenProvider
+ self.vpnTokenProvider = vpnTokenProvider
+ self.accountDetailsUseCase = accountDetailsUseCase
+ self.updateAccountUseCase = updateAccountUseCase
+ self.paymentUseCase = paymentUseCase
+ self.subscriptionsUseCase = subscriptionsUseCase
+ self.deleteAccountUseCase = deleteAccountUseCase
+ self.featureFlagsUseCase = featureFlagsUseCase
+ if let webServices = webServices {
+ customWebServices = webServices
+ } else {
+ customWebServices = nil
+ }
+ }
+
+ // MARK: AccountProvider
+
+ #if os(iOS) || os(tvOS)
+ public var planProducts: [Plan: InAppProduct]? {
+ guard let products = accessedStore.availableProducts else {
+ return nil
+ }
+ var map = [Plan: InAppProduct]()
+ for product in products {
+ guard let plan = accessedConfiguration.plan(forProductIdentifier: product.identifier) else {
+ continue
+ }
+ map[plan] = product
+ }
+ return map
+ }
+ #endif
+
+ public var isLoggedIn: Bool {
+ guard let username = accessedDatabase.secure.username() else {
+ return false
+ }
+ return (accessedDatabase.secure.password(for: username) != nil)
+ }
+
+ public var shouldCleanAccount: Bool {
+ if self.accessedDatabase.plain.accountInfo == nil,
+ self.isLoggedIn {
+ return true
+ }
+ return false
+ }
+
+ public var oldToken: String? {
+ guard let username = accessedDatabase.secure.username() else {
+ return nil
+ }
+ return accessedDatabase.secure.token(for: accessedDatabase.secure.tokenKey(for: username))
+ }
+
+ public var apiToken: String? {
+ let apiToken = apiTokenProvider.getAPIToken()
+ return apiToken?.apiToken
+ }
+
+ public var vpnToken: String? {
+ guard let vpnToken = vpnTokenProvider.getVpnToken() else {
+ return nil
+ }
+ let vpnTokenString = "vpn_token_\(vpnToken.vpnUsernameToken):\(vpnToken.vpnPasswordToken)"
+ return vpnTokenString
+ }
+
+ public var vpnTokenUsername: String? {
+ return getVpnTokenUsernameAndPassword()?.username
+ }
+
+ public var vpnTokenPassword: String? {
+ return getVpnTokenUsernameAndPassword()?.password
+ }
+
+ public var publicUsername: String? {
+ guard let username = accessedDatabase.secure.publicUsername() else {
+ return nil
+ }
+ return username
+ }
+
+ public var currentUser: UserAccount? {
+ get {
+ guard let username = accessedDatabase.secure.username() else {
+ return nil
+ }
+ guard let password = accessedDatabase.secure.password(for: username) else {
+ return nil
+ }
+ return UserAccount(
+ credentials: Credentials(username: username, password: password),
+ info: accessedDatabase.plain.accountInfo
+ )
+ }
+ set {
+ if let user = newValue {
+ accessedDatabase.secure.setPublicUsername(user.credentials.username)
+ accessedDatabase.secure.setPassword(user.credentials.password, for: user.credentials.username)
+ accessedDatabase.plain.accountInfo = user.info
+ } else {
+ if let username = accessedDatabase.secure.username() {
+ accessedDatabase.secure.setPassword(nil, for: username)
+ accessedDatabase.secure.setUsername(nil)
+ }
+ accessedDatabase.secure.setPublicUsername(nil)
+ accessedDatabase.plain.accountInfo = nil
+ }
+ }
+ }
+
+ public var currentPasswordReference: Data? {
+ guard let username = accessedDatabase.secure.username() else {
+ return nil
+ }
+ return accessedDatabase.secure.passwordReference(for: username)
+ }
+
+ #if os(iOS) || os(tvOS)
+ public var lastSignupRequest: SignupRequest? {
+ guard let email = accessedDatabase.plain.lastSignupEmail else {
+ return nil
+ }
+ return SignupRequest(email: email)
+ }
+ #endif
+
+ private func updateUsernamePassword() {
+ if let token = self.vpnToken {
+ let tokenComponents = token.components(separatedBy: ":")
+ if let username = tokenComponents.first,
+ let password = tokenComponents.last {
+ self.accessedDatabase.secure.setUsername(username)
+ self.accessedDatabase.secure.setPassword(password, for: username)
+ }
+ }
+ }
+
+ public func migrateOldTokenIfNeeded(_ callback: SuccessLibraryCallback?) {
+
+ // If it was already migrated
+ if (self.accessedDatabase.plain.tokenMigrated) {
+ callback?(nil)
+ return
+ }
+
+ // If there is something persisted. Try to migrate it.
+ if let token = oldToken {
+ webServices.migrateToken(token: token) { [weak self] (error) in
+ guard error == nil else {
+ callback?(error)
+ return
+ }
+
+ guard let username = self?.vpnTokenUsername, let password = self?.vpnTokenPassword else {
+ preconditionFailure()
+ }
+
+ self?.accessedDatabase.secure.setPassword(password, for: username)
+ self?.accessedDatabase.plain.tokenMigrated = true
+ callback?(nil)
+ }
+ } else {
+
+ // Nothing persisted. Continue.
+ callback?(nil)
+ }
+ }
+
+ public func login(with receiptRequest: LoginReceiptRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
+
+ guard !isLoggedIn else {
+ preconditionFailure()
+ }
+
+ loginUseCase.login(with: receiptRequest.receipt) { error in
+ DispatchQueue.main.async {
+ let credentials = Credentials(username: "", password: "")
+ self.handleLoginResult(error: error?.asClientError(), credentials: credentials, callback: callback)
+ }
+ }
+
+ }
+
+ public func login(with linkToken: String, _ callback: ((UserAccount?, Error?) -> Void)?) {
+ guard !isLoggedIn else {
+ preconditionFailure()
+ }
+
+ self.webServices.migrateToken(token: linkToken) { (error) in
+ let credentials = Credentials(username: "", password: "")
+ self.handleLoginResult(error: error, credentials: credentials, callback: callback)
+ }
+ }
+
+ public func login(with request: LoginRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
+ guard !isLoggedIn else {
+ preconditionFailure()
+ }
+
+ loginWithCredentials(request.credentials, callback: callback)
+
+ }
+
+ private func loginWithCredentials(_ credentials: Credentials, notificationToSend: Notification.Name = .PIAAccountDidLogin, callback: ((UserAccount?, Error?) -> Void)?) {
+
+ loginUseCase.login(with: credentials) { error in
+ DispatchQueue.main.async {
+ self.handleLoginResult(error: error?.asClientError(), credentials: credentials, notificationToSend: notificationToSend, callback: callback)
+ }
+ }
+ }
+
+ private func handleLoginResult(error: Error?, credentials: Credentials, notificationToSend: Notification.Name = .PIAAccountDidLogin, callback: ((UserAccount?, Error?) -> Void)?) {
+ guard error == nil else {
+ callback?(nil, error)
+ return
+ }
+
+ guard vpnToken != nil else {
+ callback?(nil, ClientError.unauthorized)
+ return
+ }
+
+ self.updateUser(credentials: credentials) { userAccount, error in
+ if let userAccount = userAccount {
+ Macros.postNotification(notificationToSend, [.user: userAccount])
+ }
+ callback?(userAccount, error)
+ }
+ }
+
+ private func updateUser(credentials: Credentials, callback: ((UserAccount?, Error?) -> Void)? ) {
+ self.updateUsernamePassword()
+ self.updateUserAccount(credentials: credentials, callback: callback)
+ }
+
+ private func updateUserAccount(credentials: Credentials, callback: ((UserAccount?, Error?) -> Void)?) {
+ accountDetailsUseCase() { result in
+
+ switch result {
+ case .failure(let error):
+ self.logout(nil)
+ self.cleanDatabase()
+ DispatchQueue.main.async {
+ callback?(nil, ClientError.unauthorized)
+ }
+ case .success(let accountInfo):
+ self.accessedDatabase.plain.accountInfo = accountInfo
+ self.accessedDatabase.secure.setPublicUsername(accountInfo.username)
+ let userAccount = UserAccount(credentials: credentials, info: accountInfo)
+ DispatchQueue.main.async {
+ callback?(userAccount, nil)
+ }
+ }
+ }
+
+ }
+
+ public func refreshAccountInfo(_ callback: ((AccountInfo?, Error?) -> Void)?) {
+ guard isLoggedIn,
+ let _ = self.publicUsername else {
+ guard let user = currentUser else {
+ preconditionFailure()
+ }
+
+ self.logout(nil)
+ return
+ }
+ accountInfoWith(callback)
+ }
+
+ public func accountInformation(_ callback: ((AccountInfo?, Error?) -> Void)?) {
+ guard isLoggedIn else {
+ callback?(nil, ClientError.unauthorized)
+ return
+ }
+ accountInfoWith(callback)
+ }
+
+ private func accountInfoWith(_ callback: ((AccountInfo?, Error?) -> Void)?) {
+
+ accountDetailsUseCase() { result in
+ switch result {
+ case .failure(let error):
+ DispatchQueue.main.async {
+ callback?(nil, error.asClientError())
+ }
+ case .success(let accountInfo):
+ DispatchQueue.main.async {
+ self.accessedDatabase.plain.accountInfo = accountInfo
+
+ Macros.postNotification(.PIAAccountDidRefresh, [.accountInfo: accountInfo])
+ callback?(accountInfo, nil)
+ }
+
+ }
+ }
+
+ }
+
+ public func update(with request: UpdateAccountRequest, resetPassword reset: Bool, andPassword password: String, _ callback: ((AccountInfo?, Error?) -> Void)?) {
+
+ let credentials = Credentials(username: Client.providers.accountProvider.publicUsername ?? "",
+ password: password)
+
+ if reset {
+ updateAccountUseCase.setEmail(email: request.email, resetPassword: reset) { result in
+ DispatchQueue.main.async {
+ self.handleUpdateAccountResult(result, request: request, shouldUpdatePassword: true, callback: callback)
+ }
+ }
+ } else {
+ updateAccountUseCase.setEmail(username: credentials.username, password: credentials.password, email: request.email, resetPassword: reset) { result in
+ DispatchQueue.main.async {
+ //We use the email and the password returned by the signup endpoint in the previous step, we don't update the password
+ self.handleUpdateAccountResult(result, request: request, shouldUpdatePassword: false, callback: callback)
+ }
+
+ }
+ }
+
+ }
+
+ private func handleUpdateAccountResult(_ result: Result, request: UpdateAccountRequest, shouldUpdatePassword: Bool, callback: ((AccountInfo?, Error?) -> Void)?) {
+ switch result {
+ case .failure(let error):
+ callback?(nil, error.asClientError())
+ case .success(let tempPassword):
+ if shouldUpdatePassword {
+ if let newPassword = tempPassword {
+ Client.configuration.tempAccountPassword = newPassword
+ }
+ }
+
+ self.handleUpdateAccountSuccessRequest(request, callback: callback)
+ }
+ }
+
+ private func handleUpdateAccountSuccessRequest(_ request: UpdateAccountRequest, callback: ((AccountInfo?, Error?) -> Void)?) {
+
+ guard let user = currentUser else {
+ preconditionFailure()
+ }
+
+ guard let newAccountInfo = user.info?.with(email: request.email) else {
+ Macros.postNotification(.PIAAccountDidUpdate)
+ callback?(nil, nil)
+ return
+ }
+
+ self.accessedDatabase.plain.accountInfo = newAccountInfo
+ Macros.postNotification(.PIAAccountDidUpdate, [
+ .accountInfo: newAccountInfo
+ ])
+
+ callback?(newAccountInfo, nil)
+
+ }
+
+ public func logout(_ callback: SuccessLibraryCallback?) {
+ logoutUseCase() { [weak self] error in
+ DispatchQueue.main.async {
+ self?.cleanDatabase()
+ Macros.postNotification(.PIAAccountDidLogout)
+ callback?(nil)
+ }
+
+ }
+ }
+
+ public func deleteAccount(_ callback: SuccessLibraryCallback?) {
+ guard isLoggedIn else {
+ preconditionFailure()
+ }
+
+ deleteAccountUseCase() { error in
+ DispatchQueue.main.async {
+ callback?(error?.asClientError())
+ }
+
+ }
+
+ }
+
+ public func featureFlags(_ callback: SuccessLibraryCallback?) {
+ featureFlagsUseCase() { result in
+ switch result {
+ case .failure(let error):
+ DispatchQueue.main.async {
+ callback?(error.asClientError())
+ }
+ case .success(let featuresInfo):
+ DispatchQueue.main.async {
+ Client.configuration.featureFlags.removeAll()
+ Client.configuration.featureFlags.append(contentsOf: featuresInfo.flags)
+ Macros.postNotification(Notification.Name.__AppDidFetchFeatureFlags)
+ callback?(nil)
+ }
+ }
+ }
+ }
+
+ #if os(iOS) || os(tvOS)
+ public func subscriptionInformation(_ callback: LibraryCallback?) {
+ log.debug("Fetching available product keys...")
+
+ subscriptionsUseCase(receiptBase64: nil) { result in
+ switch result {
+ case .failure(let error):
+ log.debug("SubscriptionsUseCase executed with error: \(error)")
+ DispatchQueue.main.async {
+ callback?(nil, error.asClientError())
+ }
+ case .success(let appStoreInformation):
+ DispatchQueue.main.async {
+ if let info = appStoreInformation {
+ callback?(info, nil)
+ } else {
+ log.debug("SubscriptionUseCase executed without error but unable to decode app store information")
+ callback?(nil, ClientError.malformedResponseData)
+ }
+ }
+ }
+
+ }
+
+ }
+
+ public func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
+ log.debug("Fetching available products...")
+
+ if let products = planProducts {
+ log.debug("Available products in cache: \(products)")
+ Macros.postNotification(.__InAppDidFetchProducts, [.products: products])
+ callback?(products, nil)
+ return
+ }
+
+ log.debug("No available products in cache, requesting from store...")
+
+ let identifiers = accessedConfiguration.allProductIdentifiers()
+ accessedStore.fetchProducts(identifiers: identifiers) { (products, error) in
+ let products = self.planProducts ?? [:]
+ log.debug("Available products from store: \(products)")
+ Macros.postNotification(.__InAppDidFetchProducts, [.products: products])
+ callback?(products, nil)
+ }
+ }
+
+ public func purchase(plan: Plan, _ callback: ((InAppTransaction?, Error?) -> Void)?) {
+ listPlanProducts { (map, error) in
+ guard let product = map?[plan] else {
+ callback?(nil, ClientError.productUnavailable)
+ return
+ }
+
+ self.accessedStore.purchaseProduct(product) { (transaction, error) in
+ guard let transaction = transaction else {
+ callback?(nil, error)
+ return
+ }
+ callback?(transaction, nil)
+ }
+ }
+ }
+
+ public func restorePurchases(_ callback: SuccessLibraryCallback?) {
+ accessedStore.refreshPaymentReceipt(callback)
+ }
+
+ public func loginUsingMagicLink(withEmail email: String, _ callback: SuccessLibraryCallback?) {
+ loginUseCase.loginLink(with: email) { error in
+ DispatchQueue.main.async {
+ callback?(error?.asClientError())
+ }
+
+ }
+ }
+
+ public func signup(with request: SignupRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
+ guard !isLoggedIn else {
+ preconditionFailure()
+ }
+ guard let signup = request.signup(withStore: accessedStore) else {
+ callback?(nil, ClientError.noReceipt)
+ return
+ }
+
+ accessedDatabase.plain.lastSignupEmail = request.email
+
+ signupUseCase(signup: signup) { [weak self] result in
+ guard let self else { return }
+ switch result {
+ case .success(let credentials):
+ handleSignupSuccessResult(transaction: request.transaction,
+ credentials: credentials,
+ callback: callback)
+ case .failure(let error):
+ handleSignupErrorResult(error: error.asClientError(), callback: callback)
+ }
+ }
+ }
+
+ private func handleSignupErrorResult(error: ClientError?, callback: ((UserAccount?, Error?) -> Void)?) {
+ guard error == .badReceipt, let products = Client.store.availableProducts else {
+ DispatchQueue.main.async {
+ callback?(nil, error)
+ }
+ return
+ }
+
+ for product in products {
+ if let uncreditedTransaction = Client.store.uncreditedTransaction(for: product) {
+ self.accessedStore.finishTransaction(uncreditedTransaction, success: false)
+ }
+ }
+
+ DispatchQueue.main.async {
+ callback?(nil, error)
+ }
+ }
+
+ private func handleSignupSuccessResult(transaction: InAppTransaction?, credentials: Credentials, callback: ((UserAccount?, Error?) -> Void)?) {
+
+ if let transaction = transaction {
+ self.accessedStore.finishTransaction(transaction, success: true)
+ }
+
+ self.accessedDatabase.plain.lastSignupEmail = nil
+ self.accessedDatabase.secure.setPublicUsername(credentials.username)
+ self.accessedDatabase.secure.setUsername(credentials.username)
+ self.accessedDatabase.secure.setPassword(credentials.password, for: credentials.username)
+
+ self.loginWithCredentials(credentials, notificationToSend: .PIAAccountDidSignup, callback: callback)
+ }
+
+ public func listRenewablePlans(_ callback: (([Plan]?, Error?) -> Void)?) {
+ guard let info = currentUser?.info else {
+ preconditionFailure()
+ }
+
+ listPlanProducts { (_, error) in
+ guard error == nil else {
+ callback?(nil, error)
+ return
+ }
+ guard info.isRenewable else {
+ //We need to check if the plan is a trial even when the plan is not renewable, as the
+ //error message should be different for each scenario
+ if info.plan == .trial {
+ callback?(nil, ClientError.renewingTrial)
+ } else {
+ callback?(nil, ClientError.renewingNonRenewable)
+ }
+ return
+ }
+
+ switch info.plan {
+ case .trial:
+ callback?(nil, ClientError.renewingTrial)
+ return
+ case .monthly:
+ callback?([.monthly], nil)
+ case .yearly:
+ callback?([.yearly], nil)
+ case .other:
+ callback?(nil, ClientError.renewingNonRenewable)
+ }
+ }
+ }
+
+ public func renew(with request: RenewRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
+ guard isLoggedIn else {
+ preconditionFailure()
+ }
+ guard let user = currentUser else {
+ preconditionFailure()
+ }
+ guard let accountInfo = user.info, accountInfo.isRenewable else {
+ preconditionFailure()
+ }
+ guard let payment = request.payment(withStore: accessedStore) else {
+ callback?(nil, ClientError.noReceipt)
+ return
+ }
+
+ paymentUseCase(with: user.credentials, request: payment) { (error) in
+
+ log.debug("Payment processed with error: \(error)")
+
+ DispatchQueue.main.async {
+ if let error {
+ callback?(nil, error)
+ return
+ }
+
+ if let transaction = request.transaction {
+ self.accessedStore.finishTransaction(transaction, success: true)
+ }
+
+ Macros.postNotification(.PIAAccountDidRenew)
+ }
+
+
+ self.accountDetailsUseCase() { result in
+ switch result {
+ case .success(let newAccountInfo):
+ DispatchQueue.main.async {
+ self.accessedDatabase.plain.accountInfo = newAccountInfo
+ let user = UserAccount(credentials: user.credentials, info: newAccountInfo)
+ Macros.postNotification(.PIAAccountDidRefresh, [.user: user])
+ callback?(user, nil)
+ }
+
+ case .failure(_):
+ DispatchQueue.main.async {
+ callback?(nil, nil)
+ }
+ }
+ }
+
+ }
+ }
+
+ /**
+ Remove all data from the plain and secure internal database
+ */
+ public func cleanDatabase() {
+ if let username = accessedDatabase.secure.username() {
+ accessedDatabase.secure.setPassword(nil, for: username)
+ accessedDatabase.secure.setUsername(nil)
+ accessedDatabase.secure.clear(for: username)
+ }
+ accessedDatabase.secure.removeDIPTokens()
+ accessedDatabase.secure.setPublicUsername(nil)
+ accessedDatabase.plain.accountInfo = nil
+ accessedDatabase.plain.visibleTiles = AvailableTiles.defaultTiles()
+ accessedDatabase.plain.orderedTiles = AvailableTiles.defaultTiles()
+ accessedDatabase.plain.historicalServers = []
+ accessedDatabase.plain.reset()
+ }
+
+ #endif
+
+ // MARK: WebServicesConsumer
+
+ var webServices: WebServices {
+ return customWebServices ?? accessedWebServices
+ }
+
+ public func isAPIEndpointAvailable(_ callback: LibraryCallback?) {
+ webServices.taskForConnectivityCheck { (_, error) in
+ callback?(error == nil, error)
+ }
+ }
+
+ // MARK: Private
+
+ /// :nodoc:
+ func getVpnTokenUsernameAndPassword() -> (username: String, password: String)? {
+ let token = Client.providers.accountProvider.vpnToken
+ guard let unwrappedToken = token else {
+ return nil
+ }
+
+ let tokenComponents = unwrappedToken.components(separatedBy: ":")
+ guard tokenComponents.count == 2 else {
+ return nil
+ }
+
+ guard let username = tokenComponents.first,
+ let password = tokenComponents.last else {
+ return nil
+ }
+
+ return (username, password)
+ }
+}