mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
356 lines
14 KiB
Swift
356 lines
14 KiB
Swift
//
|
|
// BalancesViewController.swift
|
|
// Multisig
|
|
//
|
|
// Created by Dmitry Bespalov on 22.10.20.
|
|
// Copyright © 2020 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
// Loads and displays balances
|
|
class BalancesViewController: LoadableViewController, UITableViewDelegate, UITableViewDataSource {
|
|
|
|
private enum Section {
|
|
case importKeyBanner
|
|
case passcodeBanner
|
|
case balances(items: [TokenBalance])
|
|
}
|
|
var clientGatewayService: BalancesAPI = App.shared.clientGatewayService
|
|
var remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.shared
|
|
var createPasscodeFlow: CreatePasscodeFlow!
|
|
|
|
override var isEmpty: Bool {
|
|
var result = false
|
|
if sections.isEmpty {
|
|
result = true
|
|
} else if sections.count == 1 {
|
|
if let firstItem = sections.first {
|
|
switch firstItem {
|
|
case .balances(let items):
|
|
result = isEmptyAccount(items: items)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
private var currentDataTask: URLSessionTask?
|
|
|
|
private var sections: [Section] = []
|
|
|
|
private let tableBackgroundColor: UIColor = .backgroundPrimary
|
|
|
|
private var shouldShowImportKeyBanner: Bool {
|
|
importKeyBannerWasShown != true && !shouldShowSafeTokenBanner
|
|
}
|
|
|
|
private var importKeyBannerWasShown: Bool? {
|
|
get { AppSettings.importKeyBannerWasShown }
|
|
set { AppSettings.importKeyBannerWasShown = newValue }
|
|
}
|
|
|
|
private var shouldShowPasscodeBanner: Bool {
|
|
OwnerKeyController.hasPrivateKey && AppSettings.shouldOfferToSetupPasscode && !shouldShowSafeTokenBanner
|
|
}
|
|
|
|
private var shouldShowSafeTokenBanner: Bool {
|
|
guard let safe = try? Safe.getSelected() else {
|
|
return false
|
|
}
|
|
return safeTokenBannerWasShown != true && ClaimingAppController.isAvailable(chain: safe.chain!)
|
|
}
|
|
|
|
private var safeTokenBannerWasShown: Bool? {
|
|
get { AppSettings.safeTokenBannerWasShown }
|
|
set { AppSettings.safeTokenBannerWasShown = newValue }
|
|
}
|
|
|
|
convenience init() {
|
|
self.init(namedClass: Self.superclass())
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
tableView.registerCell(BalanceTableViewCell.self)
|
|
tableView.registerCell(BannerTableViewCell.self)
|
|
|
|
tableView.rowHeight = UITableView.automaticDimension
|
|
tableView.estimatedRowHeight = 60
|
|
tableView.backgroundColor = tableBackgroundColor
|
|
|
|
tableView.delegate = self
|
|
tableView.dataSource = self
|
|
|
|
if importKeyBannerWasShown != true && OwnerKeyController.hasPrivateKey {
|
|
importKeyBannerWasShown = true
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(ownerKeyImported), name: .ownerKeyImported, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(updatePasscodeBanner), name: .passcodeCreated, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(lazyReloadData), name: .selectedFiatCurrencyChanged, object: nil)
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(lazyReloadData), name: .chainInfoChanged, object: nil)
|
|
|
|
// update the balances for the case when collectibles were shown and this controller was off screen.
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(doReloadData), name: .selectedSafeChanged, object: nil)
|
|
|
|
recreateSectionsWithCurrentItems()
|
|
}
|
|
|
|
@objc private func doReloadData() {
|
|
guard (try? Safe.getSelected())?.safeStatus == .deployed else { return }
|
|
reloadData()
|
|
}
|
|
|
|
@objc private func ownerKeyImported() {
|
|
importKeyBannerWasShown = true
|
|
recreateSectionsWithCurrentItems()
|
|
}
|
|
|
|
@objc private func updatePasscodeBanner() {
|
|
recreateSectionsWithCurrentItems()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
Tracker.trackEvent(.assetsCoins)
|
|
}
|
|
|
|
override func reloadData() {
|
|
super.reloadData()
|
|
currentDataTask?.cancel()
|
|
do {
|
|
if let safe = try? Safe.getSelected(), safe.chain?.isSupported(feature: .moonpay) ?? false {
|
|
emptyView.setTitle("Add some crypto to get started")
|
|
emptyView.setDescription("Buy crypto directly with your credit card or a bank account")
|
|
emptyView.setAction(text: "Buy crypto", action: { [weak self] in
|
|
guard let safe = try? Safe.getSelected() else {
|
|
return
|
|
}
|
|
|
|
Tracker.trackEvent(.userBuyCrypto)
|
|
let vc = ViewControllerFactory.selectTopUpAddress(safe: safe)
|
|
|
|
self?.present(vc, animated: true)
|
|
})
|
|
} else {
|
|
emptyView.setTitle("Balances will appear here")
|
|
emptyView.setDescription(nil)
|
|
emptyView.setAction(text: nil) { }
|
|
}
|
|
guard let safe = try Safe.getSelected() else {
|
|
return
|
|
}
|
|
NotificationCenter.default.post(
|
|
name: .balanceLoading,
|
|
object: self
|
|
)
|
|
currentDataTask = clientGatewayService.asyncBalances(safeAddress: safe.addressValue,
|
|
chainId: safe.chain!.id!) { [weak self] result in
|
|
guard let `self` = self else { return }
|
|
switch result {
|
|
case .failure(let error):
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let `self` = self else { return }
|
|
// ignore cancellation error due to cancelling the
|
|
// currently running task. Otherwise user will see
|
|
// meaningless message.
|
|
if (error as NSError).code == URLError.cancelled.rawValue &&
|
|
(error as NSError).domain == NSURLErrorDomain {
|
|
return
|
|
}
|
|
self.onError(GSError.error(description: "Failed to load balances", error: error))
|
|
}
|
|
case .success(let summary):
|
|
DispatchQueue.main.async { [weak self] in
|
|
let results = summary.items.map { TokenBalance($0, code: AppSettings.selectedFiatCode) }
|
|
let total = TokenBalance.displayCurrency(from: summary.fiatTotal, code: AppSettings.selectedFiatCode)
|
|
guard let `self` = self else { return }
|
|
|
|
//update coins total balance header by propagating total value and balances
|
|
//propagation of balances is needed to init SelectAssetViewController from AssetsViewController when send button is clicked
|
|
NotificationCenter.default.post(
|
|
name: .balanceUpdated,
|
|
object: self,
|
|
userInfo: ["balances": results, "total": total]
|
|
)
|
|
self.sections = self.makeSections(items: results)
|
|
self.onSuccess()
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
onError(GSError.error(description: "Failed to load balances", error: error))
|
|
}
|
|
}
|
|
|
|
private func makeSections(items: [TokenBalance]) -> [Section] {
|
|
guard !isEmptyAccount(items: items) else { return [] }
|
|
|
|
var sections = [Section]()
|
|
|
|
if shouldShowImportKeyBanner {
|
|
sections.append(.importKeyBanner)
|
|
} else if shouldShowPasscodeBanner {
|
|
sections.append(.passcodeBanner)
|
|
}
|
|
|
|
sections.append(.balances(items: items))
|
|
return sections
|
|
}
|
|
|
|
private func isEmptyAccount(items: [TokenBalance]) -> Bool {
|
|
items.isEmpty || items.count == 1 && items.first?.balance == "0"
|
|
}
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
sections.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
switch sections[section] {
|
|
case .importKeyBanner, .passcodeBanner: return 1
|
|
case .balances(items: let items): return items.count
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
guard indexPath.section < sections.count else { return UITableViewCell() }
|
|
switch sections[indexPath.section] {
|
|
case .importKeyBanner:
|
|
return importKeyBanner(indexPath: indexPath)
|
|
case .passcodeBanner:
|
|
return createPasscodeBanner(indexPath: indexPath)
|
|
case .balances(items: let items):
|
|
guard indexPath.row < items.count else { return UITableViewCell() }
|
|
let item = items[indexPath.row]
|
|
let cell = tableView.dequeueCell(BalanceTableViewCell.self, for: indexPath)
|
|
cell.setMainText(item.symbol)
|
|
cell.setDetailText(item.balance)
|
|
cell.setSubDetailText(item.fiatBalance)
|
|
cell.setBrowsingEnabled(item.address != TokenBalance.nativeTokenAddress)
|
|
if let image = item.image {
|
|
cell.setImage(image)
|
|
} else {
|
|
cell.setImage(with: item.imageURL, placeholder: UIImage(named: "ico-token-placeholder")!)
|
|
}
|
|
return cell
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
switch sections[indexPath.section] {
|
|
case .balances(items: let items):
|
|
do {
|
|
let item = items[indexPath.row]
|
|
guard let safe = try Safe.getSelected(), item.address != TokenBalance.nativeTokenAddress else { return }
|
|
openInSafari(safe.chain!.browserURL(address: item.address))
|
|
} catch {
|
|
App.shared.snackbar.show(
|
|
error: GSError.error(description: "Failed to update selected safe", error: error))
|
|
}
|
|
default: break
|
|
}
|
|
}
|
|
|
|
private func showSend(balance: TokenBalance) {
|
|
Tracker.trackEvent(.assetsTransferSelectedAsset)
|
|
let transferFundsVC = TransactionViewController()
|
|
transferFundsVC.tokenBalance = balance
|
|
let ribbon = ViewControllerFactory.ribbonWith(viewController: transferFundsVC)
|
|
present(ViewControllerFactory.modal(viewController: ribbon), animated: true)
|
|
}
|
|
|
|
private func importKeyBanner(indexPath: IndexPath) -> UITableViewCell {
|
|
let cell = tableView.dequeueCell(BannerTableViewCell.self, for: indexPath)
|
|
cell.setHeader(ImportKeyBanner.Strings.header)
|
|
cell.setBody(ImportKeyBanner.Strings.body)
|
|
cell.setButton(ImportKeyBanner.Strings.button)
|
|
cell.onClose = { [unowned self] in
|
|
importKeyBannerWasShown = true
|
|
|
|
recreateSectionsWithCurrentItems()
|
|
|
|
Tracker.trackEvent(.bannerImportOwnerKeySkipped)
|
|
}
|
|
cell.onImport = { [unowned self] in
|
|
importKeyBannerWasShown = true
|
|
|
|
recreateSectionsWithCurrentItems()
|
|
|
|
let vc = ViewControllerFactory.addOwnerViewController {
|
|
self.dismiss(animated: true, completion: nil)
|
|
}
|
|
present(vc, animated: true)
|
|
Tracker.trackEvent(.bannerImportOwnerKeyAdd)
|
|
}
|
|
cell.selectionStyle = .none
|
|
return cell
|
|
}
|
|
|
|
private func createPasscodeBanner(indexPath: IndexPath) -> UITableViewCell {
|
|
let cell = tableView.dequeueCell(BannerTableViewCell.self, for: indexPath)
|
|
cell.setHeader(PasscodeBanner.Strings.header)
|
|
cell.setBody(PasscodeBanner.Strings.body)
|
|
cell.setButton(PasscodeBanner.Strings.button)
|
|
cell.onClose = { [unowned self] in
|
|
AppSettings.passcodeBannerDismissed = true
|
|
recreateSectionsWithCurrentItems()
|
|
Tracker.trackEvent(.skipPasscodeBanner)
|
|
}
|
|
cell.onImport = { [unowned self] in
|
|
AppSettings.passcodeBannerDismissed = true
|
|
recreateSectionsWithCurrentItems()
|
|
|
|
createPasscodeFlow = CreatePasscodeFlow(completion: { [unowned self] _ in
|
|
createPasscodeFlow = nil
|
|
recreateSectionsWithCurrentItems()
|
|
})
|
|
present(flow: createPasscodeFlow)
|
|
|
|
Tracker.trackEvent(.setupPasscodeFromBanner)
|
|
}
|
|
cell.selectionStyle = .none
|
|
return cell
|
|
}
|
|
|
|
private func recreateSectionsWithCurrentItems() {
|
|
var items = [TokenBalance]()
|
|
for section in sections {
|
|
switch section {
|
|
case .balances(items: let balances): items = balances
|
|
default: continue
|
|
}
|
|
}
|
|
sections = makeSections(items: items)
|
|
tableView.reloadData()
|
|
}
|
|
}
|
|
|
|
extension BalancesViewController {
|
|
enum ImportKeyBanner {
|
|
enum Strings {
|
|
static let header = "Add owner key"
|
|
static let body = "Did you know that you can import your owner key to sign and execute transactions on the go?"
|
|
static let button = "Add owner key now"
|
|
}
|
|
}
|
|
|
|
enum PasscodeBanner {
|
|
enum Strings {
|
|
static let header = "Create passcode"
|
|
static let body = "Secure your owner keys by setting up a passcode. The passcode will be needed to open the app and sign transactions."
|
|
static let button = "Create passcode now"
|
|
}
|
|
}
|
|
}
|