// // AccountsListController.swift // Malinka // // Created by NUT.Tech on 26.08.2022. // Copyright © 2022 NUT.Tech. All rights reserved. // import UIKit import WalletFoundation import WalletKit import Combine enum AccountStatus: Equatable { case accepted(selected: Bool) case creating(selected: Bool) case pending case declined init(wallet: WalletKit.Wallet, selected: Bool) { switch wallet.state { case .accepted: self = .accepted(selected: selected) case .creating: self = .creating(selected: selected) case .pending: self = .pending case .declined: self = .declined } } } final class AccountsListController: UIViewController { private enum Constants { enum Geometry { static let fontSize: CGFloat = 14 static let buttonCornerRadius: CGFloat = 5 static let buttonHeight: CGFloat = 48 static let buttonMargin: CGFloat = 12 static let buttonTitlePadding: CGFloat = 5 static let stackMargin: CGFloat = 16 } } private var countBeforeCreated: Int? private var store = Set() private lazy var scrollView: UIScrollView = { let scrollview = UIScrollView() scrollview.translatesAutoresizingMaskIntoConstraints = false return scrollview }() private lazy var addAccountButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(L10n.Main.Account.add, for: .normal) button.setTitleColor(Asset.deepWater.color, for: .normal) button.setImage(Asset.chatsAddPlus.image, for: .normal) button.clipsToBounds = true if let font = FontFamily.GolosUI.medium.font(size: Constants.Geometry.fontSize) { button.titleLabel?.font = font } button.layer.cornerRadius = Constants.Geometry.buttonCornerRadius let borderColor = Asset.deepWater.color.withAlphaComponent(0.2) button.layer.borderColor = borderColor.cgColor button.layer.borderWidth = 1.0 button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: Constants.Geometry.buttonTitlePadding) button.titleEdgeInsets = UIEdgeInsets(top: 0, left: Constants.Geometry.buttonTitlePadding, bottom: 0, right: -Constants.Geometry.buttonTitlePadding) return button }() private lazy var accountsStack: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = Constants.Geometry.stackMargin return stackView }() private var viewModel: AccountsListViewModel? override func viewDidLoad() { super.viewDidLoad() self.title = L10n.Main.Account.title self.navigationItem.leftBarButtonItem = .pop(self, { vc in vc.dismiss(animated: true) }) self.setupViewModel() self.setupViews() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.viewModel?.isLocked(true) self.navigationController >>- { $0.navigationBar.isTranslucent = false $0.navigationBar.backgroundColor = Asset.snow.color $0.setNavigationBarHidden(false, animated: true) } self.viewModel?.refreshAccountsStates() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.viewModel?.isLocked(false) } private func setupViewModel() { let viewModel = AccountsListViewModel() self.viewModel = viewModel viewModel.walletsPublisher // notifies about changes of accounts(wallets) array (number of accounts for this device) .receive(on: DispatchQueue.main) .sink { [weak self] wallets in guard let self else { return } self.accountsViewBehaviour(for: wallets) } .store(in: &self.store) viewModel.updatePublisher // notifies about changes of accounts(wallets) states .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } self.accountsViewBehaviour(for: self.viewModel?.accounts ?? []) } .store(in: &self.store) viewModel.activePublisher // notifies about active account(wallet) changes .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } if !self.removeModalRequired() { self.refreshViews() } } .store(in: &self.store) NotificationCenter.default .publisher(for: UIApplication.willEnterForegroundNotification) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } self.viewModel?.refreshAccountsStates() } .store(in: &self.store) if self.viewModel?.selected == nil, let wallet = self.viewModel?.accounts.last(where: { $0.isOnActiveState }) { self.viewModel?.activate(wallet: wallet) } } private func accountsViewBehaviour(for wallets: [WalletKit.Wallet]) { guard let countBeforeCreated = self.countBeforeCreated else { if wallets.count == 1, let wallet = wallets.first, wallet.isOnActiveState { self.viewModel?.activate(wallet: wallet) } self.refreshViews() return } if countBeforeCreated != wallets.count, let wallet = wallets.last(where: { $0.isOnActiveState }) { // If previously selected exists if let selected = self.viewModel?.selected { if !wallet.isEquals(other: selected), let wIndex = wallets.firstIndex(where: { $0.isEquals(other: wallet) }), let sIndex = wallets.firstIndex(where: { $0.isEquals(other: selected) }), wIndex > sIndex { self.countBeforeCreated = nil self.viewModel?.activate(wallet: wallet) } else { self.refreshViews() } } else { // No one previously selected self.countBeforeCreated = nil self.viewModel?.activate(wallet: wallet) } } else { self.refreshViews() } } private func setupViews() { self.view.backgroundColor = Asset.snow.color self.scrollView.refreshControl = UIRefreshControl() self.scrollView.refreshControl?.addTarget(self, action: #selector(self.callPullToRefresh), for: .valueChanged) self.scrollView.addSubview(self.accountsStack) self.view.addSubview(self.scrollView) self.view.addSubview(self.addAccountButton) NSLayoutConstraint.activate([ self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor), self.scrollView.leftAnchor.constraint(equalTo: self.view.leftAnchor), self.scrollView.rightAnchor.constraint(equalTo: self.view.rightAnchor), self.accountsStack.topAnchor.constraint(equalTo: self.scrollView.topAnchor), self.accountsStack.leftAnchor.constraint(equalTo: self.scrollView.leftAnchor, constant: Constants.Geometry.stackMargin), self.accountsStack.rightAnchor.constraint(equalTo: self.scrollView.rightAnchor, constant: -Constants.Geometry.stackMargin), self.accountsStack.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor), self.accountsStack.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor, constant: -2 * Constants.Geometry.stackMargin), self.addAccountButton.heightAnchor.constraint(equalToConstant: Constants.Geometry.buttonHeight), self.addAccountButton.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: Constants.Geometry.buttonMargin), self.addAccountButton.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -Constants.Geometry.buttonMargin), self.addAccountButton.topAnchor.constraint(equalTo: self.scrollView.bottomAnchor, constant: Constants.Geometry.buttonMargin), self.addAccountButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.Geometry.buttonMargin) ]) self.addAccountButton.addTarget(self, action: #selector(addAccountPressed), for: .touchUpInside) } private func removeModalRequired() -> Bool { if let parent = self.presentingViewController, parent.presentedViewController == self, self.viewModel?.isSelectedAccountAccepted ?? false { self.dismiss(animated: true) return true } return false } private func configureView() { self.configureLeftBarButtonItem() self.accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } let accounts = self.viewModel?.accounts ?? [] accounts.forEach { account in let selected = account.name == self.viewModel?.selected?.name let status = AccountStatus(wallet: account, selected: selected) let accountItem = AccountListCard(title: account.name, keyType: account.keyType, status: status) { [weak self] in guard let self else { return } AccountController.showPrivateKeyPopup(in: self, wallet: account) } deleteAction: { [weak self] in guard let self else { return } Alert.system( text: L10n.Main.Account.remove, actions: [ .yes { self.viewModel?.remove(wallet: account) guard self.viewModel?.accounts.isEmpty == true else { return } self.mainController.content.pop(animated: true) }, .no ], in: self ) } refreshBlock: { [weak self] in guard let self else { return } UIView.animate(withDuration: .slowest) { self.accountsStack.arrangedSubviews .compactMap({ $0 as? AccountListCard }) .forEach { switch $0.status { case .accepted(selected: true): $0.status = .accepted(selected: false) case .creating(selected: true): $0.status = .creating(selected: false) default: break } } self.view.layoutIfNeeded() self.viewModel?.activate(wallet: account) if account.state == .accepted { self.mainController.content.pop(animated: true) } } } self.accountsStack.addArrangedSubview(accountItem) } self.scrollView.layoutIfNeeded() } private func configureLeftBarButtonItem() { guard let leftBarButtonItem = self.navigationItem.leftBarButtonItem else { return } let hasActiveAccounts = self.viewModel?.isSelectedAccountAccepted ?? false if #available(iOS 16.0, *) { leftBarButtonItem.isHidden = !hasActiveAccounts } leftBarButtonItem.isEnabled = hasActiveAccounts leftBarButtonItem.image?.withTintColor(hasActiveAccounts ? Asset.dark.color : Asset.disabled.color) } private func refreshViews() { self.configureView() self.scrollView.isUserInteractionEnabled = true self.scrollView.refreshControl?.endRefreshing() } @objc private func addAccountPressed() { AccountController.showPopup(in: self) { result in switch result { case .new: self.countBeforeCreated = Accounts().collection.count default: self.countBeforeCreated = nil } } } @objc private func callPullToRefresh() { self.scrollView.isUserInteractionEnabled = false self.viewModel?.refreshAccountsStates() } }