Files
raspberry/iOS/Wallet/Sources/Account/Service/AccountService.swift
T

305 lines
11 KiB
Swift

//
// AccountService.swift
// List
//
// Created by Igor Danich on 29.06.2020.
// Copyright © 2020 Igor Danich. All rights reserved.
//
import Foundation
import LocalAuthentication
import Firebase
import FirebaseMessaging
import Alamofire
import Resolver
import WalletKit
import WalletNetwork
//swiftlint:disable:next identifier_name
@discardableResult func Accounts() -> Account.Service { .shared }
extension Notification.Name {
static let didChangeAccount = Notification.Name("Account.Service.didChangeAccount")
static let didUpdateLock = Notification.Name("Account.Service.didUpdateLock")
static let didUpdateAuthorization = Notification.Name("Account.Service.didUpdateAuthorization")
static let didChangeInterface = Notification.Name("Account.Interface.didUpdateTabBar")
}
extension Common.Model.Key {
fileprivate static let current = Common.Model.Key("Account.Service.current")
fileprivate static let collection = Common.Model.Key("Account.Service.collection")
fileprivate static let didLoadApp = Common.Model.Key("Account.Service.didLoadApp")
fileprivate static let password = Common.Model.Key("Account.Service.password")
fileprivate static let isPushEnabled = Common.Model.Key("Account.Service.isPushEnabled")
fileprivate static let isBiometricksEnabled = Common.Model.Key("Account.Service.isBiometricksEnabled")
fileprivate static let isAccountMigrated = Common.Model.Key("Account.Service.isAccountMigrated")
}
extension Account {
class Service: Common.Service.Provider {
static let shared = Service()
let cityId = 16
private func account(for username: String?, keyType: KeyType) -> Account.Model? {
collection.first(where: { $0.username == username && $0.keyType == keyType })
}
private var _current: String? {
get { Settings()[.current] }
set { Settings()[.current] = newValue }
}
var current: Account.Model? {
get {
let username = _current?.components(separatedBy: "@").first
let keyTypeString = _current?.components(separatedBy: "@").last ?? ""
let keyType = Account.KeyType.init(rawValue: keyTypeString) ?? Account.KeyType.active
if let current = account(for: username, keyType: keyType) { return current }
if let account = collection.first {
_current = "\(account.username)@\(account.keyType.rawValue)"
setup()
}
return account(for: username, keyType: keyType)
}
set {
// removePush()
_current = "\(newValue?.username ?? "")@\(newValue?.keyType.rawValue ?? "")"
setup()
ResolverScope.userSession.reset()
Notification.post(name: .didChangeAccount)
}
}
var exists: Bool { current != nil }
let quota = Account.Service.CommonResources()
var isLocked = false {
didSet { Notification.post(name: .didUpdateLock) }
}
var hasActiveAccounts: Bool {
guard !self.collection.isEmpty else { return false }
let filteredAccounts = self.collection
.filter {
switch $0.state {
case .declined, .pending:
return false
default:
return true
}
}
guard !filteredAccounts.isEmpty else { return false }
return true
}
let contacts = Contacts()
private var didLoadApp: Bool {
get { Settings()[.didLoadApp] }
set { Settings()[.didLoadApp] = newValue }
}
private init() {
if !didLoadApp {
didLoadApp = true
password = nil
}
updatePush()
}
private func setup() {
updatePush()
quota.fetch(clear: true)
}
}
}
// MARK: - Edit
extension Account.Service {
private(set) var collection: [Account.Model] {
get { Settings()[.collection] ?? [] }
set { Settings()[.collection] = newValue }
}
func addAccounts(usernames: [String], privateKey: String, password: String, keyType: Account.KeyType, state: WalletState) throws {
guard let publicKey = privateKey.publicKey() else {
throw NSError.error(L10n.Error.Accounts.keyInvalid)
}
for username in usernames {
guard !collection.contains(where: { $0.username == username && $0.keyType == keyType }) else {
if usernames.count == 1 {
throw NSError.error(L10n.Error.Accounts.exists)
}
continue
}
collection.append(.init(username: username,
publicKey: publicKey,
privateKey: privateKey,
keyType: keyType,
password: password,
state: state))
}
try? Account.Service.History.shared.load(username: collection.last?.username ?? "")
current = collection.last
}
func remove(username: String, keyType: Account.KeyType) {
guard let index = collection.lastIndex(where: { $0.username == username && $0.keyType == keyType }) else { return }
let value = collection[index]
let current = self.current
collection.remove(at: index)
value.clear()
if value.username == current?.username { self.current = collection.first }
updatePush()
}
func updateCredentialsIfNeeded(accounts: [Account.Model.ChangeKey], password: String) {
let collection = self.collection
for accountToChange in accounts {
guard var account = collection.first(where: {
$0.username == accountToChange.username && $0.keyType.rawValue == accountToChange.permission.permName
}) else { continue }
account.publicKey = accountToChange.publicKey
account.updatePrivateKey(privateKey: accountToChange.privateKey, password: password)
}
self.collection = collection
}
func migrateChats(accounts: [Account.Model.ChangeKey], oldPrivateKey: String) {
for accountToChange in accounts {
guard let msgsService = try? CryptoChat.Service.MsgsHistory(username: accountToChange.username, encryptionKey: oldPrivateKey) else { continue }
msgsService.copy(username: accountToChange.username, privateKey: accountToChange.privateKey, publicKey: accountToChange.publicKey)
msgsService.removeDatabase()
}
}
func removeAll() {
collection.forEach({
remove(username: $0.username, keyType: $0.keyType)
})
}
}
// MARK: - Authorization
extension Account.Service {
private var password: String? {
get { WalletKeychain.instance[biometric: .password] }
set { WalletKeychain.instance[.password, password: newValue ?? ""] = newValue }
}
var isAuthorized: Bool {
if UserDefaults.standard.bool(forKey: "isPinPwdMode") {
if let pin = Account.Service.Authorize.shared.password,
let password = getPassword(password: pin),
!password.isEmpty,
password == pin,
password.count == 4 {
return true
} else {
return false
}
} else {
return true
}
}
func getPassword(password: String?) -> String? {
if let password = password {
return WalletKeychain.instance[.password, password: password]
} else {
return WalletKeychain.instance[biometric: .password]
}
}
var isBiometricksEnabled: Bool {
get { Settings()[.isBiometricksEnabled] }
set { Settings()[.isBiometricksEnabled] = newValue }
}
var isPasswordExists: Bool { WalletKeychain.instance.exist(.password) }
var isFaceIdAvailable: Bool { LAContext().biometryType == .faceID }
var isBiometricksAvailable: Bool {
let context = LAContext()
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) ? context.biometryType != .none : false
}
func checkBiometricks(_ completion: @escaping (Bool) -> Void) {
LAContext().evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Enable bimetricks"
) { (success, _) in
DispatchQueue.main.async { completion(success) }
}
}
func update(password: String, old: String? = nil) {
self.password = password
if let old = old {
collection.forEach { $0.update(password: password, old: old) }
}
}
func removePassword() { self.password = nil }
func validate(password: String) -> Bool { password.count >= 4 }
}
// MARK: - Push Notifications
extension Account.Service {
var isPushEnabled: Bool {
get { Settings()[.isPushEnabled] }
set { Settings()[.isPushEnabled] = newValue; updatePush() }
}
private func updatePush() {
// TODO: - Update only when have accounts!
guard let url = URL(string: ApplicationEnvironment.shared().current.paycash.paycashGraphQl),
let token = Messaging.messaging().fcmToken else {
return
}
let environment = CommonEnvironment(url: url)
let service = AccountService(enmironment: environment)
Task {
await service.updateNotification(token: token,
accounts: collection.map { $0.username },
language: Common.Model.Language.current.rawValue)
}
}
}
// MARK: - Migration
extension Account.Service {
var isAccountMigrated: Bool {
get { Settings()[.isAccountMigrated] }
set { Settings()[.isAccountMigrated] = newValue; }
}
func migrateAccountIfNeeded(password: String) {
if !isAccountMigrated,
let data = UserDefaults.standard.value(forKey: Common.Model.Key.collection.rawValue) as? Data,
let objects = try? JSONDecoder().decode([Account.OldModel].self, from: data) {
collection = []
var accounts: [Account.Model] = []
objects.forEach { oldAccount in
let account = Account.Model(username: oldAccount.username,
publicKey: oldAccount.publicKey,
keyType: .active,
state: .accepted)
account.migrate(password: password)
accounts.append(account)
// try? Account.Service.History.shared.load(username: account.username)
}
collection = accounts
Notification.post(name: .didChangeAccount)
}
isAccountMigrated = true
}
}