Files

336 lines
13 KiB
Swift

//
// 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<AnyCancellable>()
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()
}
}