Added tokens support

pull/2/head
Michael Scoff 7 years ago
parent 8b8ca20160
commit b07e158a10
  1. 12
      Trust.xcodeproj/project.pbxproj
  2. 9
      Trust/Coordinator/AppCoordinator.swift
  3. 4
      Trust/Models/Token.swift
  4. 77
      Trust/Transactions/Tokens/TokenViewCell.swift
  5. 45
      Trust/Transactions/Tokens/TokenViewCellViewModel.swift
  6. 37
      Trust/Transactions/Tokens/TokensDataStore.swift
  7. 133
      Trust/Transactions/Tokens/TokensViewController.swift
  8. 37
      Trust/Transactions/Tokens/TokensViewModel.swift
  9. 31
      Trust/Transactions/TransactionDataStore.swift
  10. 27
      Trust/Transactions/TransactionsViewController.swift
  11. 15
      TrustTests/Coordinators/AppCoordinatorTests.swift

@ -53,6 +53,9 @@
296106BF1F7639250006164B /* FetchTransactionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106BE1F7639250006164B /* FetchTransactionsRequest.swift */; };
296106C21F76403A0006164B /* TokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106C11F76403A0006164B /* TokenViewCell.swift */; };
296106C41F7640C50006164B /* TokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106C31F7640C50006164B /* TokenViewCellViewModel.swift */; };
296106C61F7645CC0006164B /* TokensViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106C51F7645CC0006164B /* TokensViewController.swift */; };
296106C81F7646590006164B /* TokensViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106C71F7646590006164B /* TokensViewModel.swift */; };
296106CA1F764AB60006164B /* TokensDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296106C91F764AB60006164B /* TokensDataStore.swift */; };
296421951F70C1EC00EB363B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296421941F70C1EC00EB363B /* LoadingView.swift */; };
296421971F70C1F200EB363B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296421961F70C1F200EB363B /* ErrorView.swift */; };
296421991F70C1F900EB363B /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296421981F70C1F900EB363B /* EmptyView.swift */; };
@ -173,6 +176,9 @@
296106BE1F7639250006164B /* FetchTransactionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTransactionsRequest.swift; sourceTree = "<group>"; };
296106C11F76403A0006164B /* TokenViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenViewCell.swift; sourceTree = "<group>"; };
296106C31F7640C50006164B /* TokenViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenViewCellViewModel.swift; sourceTree = "<group>"; };
296106C51F7645CC0006164B /* TokensViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensViewController.swift; sourceTree = "<group>"; };
296106C71F7646590006164B /* TokensViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensViewModel.swift; sourceTree = "<group>"; };
296106C91F764AB60006164B /* TokensDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensDataStore.swift; sourceTree = "<group>"; };
296421941F70C1EC00EB363B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
296421961F70C1F200EB363B /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
296421981F70C1F900EB363B /* EmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = "<group>"; };
@ -502,6 +508,9 @@
children = (
296106C11F76403A0006164B /* TokenViewCell.swift */,
296106C31F7640C50006164B /* TokenViewCellViewModel.swift */,
296106C51F7645CC0006164B /* TokensViewController.swift */,
296106C71F7646590006164B /* TokensViewModel.swift */,
296106C91F764AB60006164B /* TokensDataStore.swift */,
);
path = Tokens;
sourceTree = "<group>";
@ -968,6 +977,7 @@
291F52B91F6B880F00B369AB /* EtherKeystore.swift in Sources */,
29BB94971F6FCD60009B09CC /* SendViewModel.swift in Sources */,
291F52B31F6B814300B369AB /* SMP Bignum Extensions.swift in Sources */,
296106C81F7646590006164B /* TokensViewModel.swift in Sources */,
291F52B41F6B814300B369AB /* SMP Core.swift in Sources */,
2996F14A1F6C9D10005C33AE /* ExportCoordinator.swift in Sources */,
29BE3FD21F707DC300F6BFC2 /* TransactionCoordinator.swift in Sources */,
@ -978,6 +988,7 @@
291ED08B1F6F5D2100E7E93A /* Bundle.swift in Sources */,
296106BF1F7639250006164B /* FetchTransactionsRequest.swift in Sources */,
293B8B451F70A20200356286 /* TransactionViewCell.swift in Sources */,
296106C61F7645CC0006164B /* TokensViewController.swift in Sources */,
29282B531F7630970067F88D /* Token.swift in Sources */,
29C9F5FB1F720C050025C494 /* FloatLabelTextField.swift in Sources */,
296421951F70C1EC00EB363B /* LoadingView.swift in Sources */,
@ -1027,6 +1038,7 @@
291F52BC1F6B8D0600B369AB /* Account.swift in Sources */,
2912CD321F6A83EE00C6CBE3 /* WelcomeViewController.swift in Sources */,
291F52A91F6B7BE100B369AB /* BlockNumber.swift in Sources */,
296106CA1F764AB60006164B /* TokensDataStore.swift in Sources */,
29BB94931F6FC380009B09CC /* BalanceViewModel.swift in Sources */,
29D03F1D1F712183006E548C /* Button.swift in Sources */,
291F52BF1F6C874E00B369AB /* AccountsViewController.swift in Sources */,

@ -84,6 +84,11 @@ class AppCoordinator: NSObject {
settingsCoordinator.start()
settingsCoordinator.delegate = self
}
func showTokens(for account: Account) {
let controller = TokensViewController(account: account)
rootNavigationController.pushViewController(controller, animated: true)
}
}
extension AppCoordinator: WelcomeViewControllerDelegate {
@ -158,4 +163,8 @@ extension AppCoordinator: TransactionsViewControllerDelegate {
let controller = TransactionViewController(transaction: transaction)
rootNavigationController.pushViewController(controller, animated: true)
}
func didPressTokens(account: Account, in viewController: TransactionsViewController) {
showTokens(for: account)
}
}

@ -8,6 +8,7 @@ struct Token {
let symbol: String
let totalSupply: String
let balance: Int64
let decimals: Int64
}
extension Token {
@ -18,7 +19,8 @@ extension Token {
name: tokenInfo["name"] as? String ?? "",
symbol: tokenInfo["symbol"] as? String ?? "",
totalSupply: tokenInfo["symbol"] as? String ?? "",
balance: json["balance"] as? Int64 ?? 0
balance: json["balance"] as? Int64 ?? 0,
decimals: json["decimals"] as? Int64 ?? Int64(json["decimals"] as? String ?? "") ?? 0
)
}
}

@ -0,0 +1,77 @@
// Copyright SIX DAY LLC, Inc. All rights reserved.
import Foundation
import UIKit
class TokenViewCell: UITableViewCell {
static let identifier = "TokenViewCell"
let titleLabel = UILabel()
let amountLabel = UILabel()
let symbolImageView = UIImageView()
let subTitleLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
subTitleLabel.translatesAutoresizingMaskIntoConstraints = false
subTitleLabel.lineBreakMode = .byTruncatingMiddle
symbolImageView.translatesAutoresizingMaskIntoConstraints = false
symbolImageView.image = R.image.accountsSwitch()
amountLabel.textAlignment = .right
amountLabel.translatesAutoresizingMaskIntoConstraints = false
let leftStackView = UIStackView(arrangedSubviews: [titleLabel, subTitleLabel])
leftStackView.translatesAutoresizingMaskIntoConstraints = false
leftStackView.axis = .vertical
leftStackView.spacing = 6
let rightStackView = UIStackView(arrangedSubviews: [amountLabel])
rightStackView.translatesAutoresizingMaskIntoConstraints = false
rightStackView.axis = .vertical
let stackView = UIStackView(arrangedSubviews: [symbolImageView, leftStackView, rightStackView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 10
stackView.distribution = .fill
symbolImageView.setContentHuggingPriority(UILayoutPriorityDefaultLow, for: .horizontal)
titleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, for: .horizontal)
amountLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .horizontal)
stackView.setContentHuggingPriority(UILayoutPriorityRequired, for: .horizontal)
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: Layout.sideMargin),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.sideMargin),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -Layout.sideMargin),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.sideMargin),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: TokenViewCellViewModel) {
titleLabel.text = viewModel.title
amountLabel.text = viewModel.amount
amountLabel.textColor = viewModel.amountTextColor
amountLabel.font = viewModel.amountFont
subTitleLabel.text = viewModel.subTitle
subTitleLabel.textColor = viewModel.subTitleTextColor
subTitleLabel.font = viewModel.subTitleFont
backgroundColor = viewModel.backgroundColor
}
}

@ -0,0 +1,45 @@
// Copyright SIX DAY LLC, Inc. All rights reserved.
import Foundation
import UIKit
struct TokenViewCellViewModel {
let token: Token
init(token: Token) {
self.token = token
}
var title: String {
return token.name
}
var amount: String {
return "\(token.balance)"
}
var amountTextColor: UIColor {
return Colors.black
}
var amountFont: UIFont {
return UIFont.systemFont(ofSize: 17, weight: UIFontWeightMedium)
}
var subTitle: String {
return token.symbol
}
var subTitleTextColor: UIColor {
return Colors.gray
}
var subTitleFont: UIFont {
return UIFont.systemFont(ofSize: 12, weight: UIFontWeightThin)
}
var backgroundColor: UIColor {
return .white
}
}

@ -0,0 +1,37 @@
// Copyright SIX DAY LLC, Inc. All rights reserved.
import Foundation
import Result
import APIKit
protocol TokensDataStoreDelegate: class {
func didUpdate(result: Result<TokensViewModel, TokenError>)
}
class TokensDataStore {
let account: Account
weak var delegate: TokensDataStoreDelegate?
var tokens: [Token] = []
init(account: Account) {
self.account = account
}
func update(tokens: [Token]) {
self.tokens = tokens
delegate?.didUpdate(result: .success(TokensViewModel(tokens: tokens)))
}
func fetch() {
let request = GetTokensRequest(address: account.address.address)
Session.send(request) { result in
switch result {
case .success(let response):
self.update(tokens: response)
case .failure:
self.delegate?.didUpdate(result: .failure(TokenError.failedToFetch))
}
}
}
}

@ -0,0 +1,133 @@
// Copyright SIX DAY LLC, Inc. All rights reserved.
import Foundation
import UIKit
import StatefulViewController
import Result
class TokensViewController: UIViewController {
private lazy var dataStore: TokensDataStore = {
return .init(account: self.account)
}()
var viewModel: TokensViewModel = TokensViewModel(tokens: [])
let account: Account
let tableView: UITableView
let refreshControl = UIRefreshControl()
init(
account: Account
) {
self.account = account
tableView = UITableView(frame: .zero, style: .plain)
super.init(nibName: nil, bundle: nil)
dataStore.delegate = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.backgroundColor = .white
tableView.rowHeight = 72
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
])
refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
tableView.addSubview(refreshControl)
errorView = {
let view = ErrorView()
view.onRetry = fetch
return view
}()
loadingView = {
let view = LoadingView()
return view
}()
emptyView = {
let view = EmptyView()
view.onRetry = fetch
return view
}()
title = viewModel.title
view.backgroundColor = viewModel.backgroundColor
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetch()
}
func pullToRefresh() {
refreshControl.beginRefreshing()
fetch()
}
func fetch() {
dataStore.fetch()
startLoading()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension TokensViewController: StatefulViewController {
func hasContent() -> Bool {
return viewModel.hasContent
}
}
extension TokensViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true )
}
}
extension TokensViewController: TokensDataStoreDelegate {
func didUpdate(result: Result<TokensViewModel, TokenError>) {
switch result {
case .success(let viewModel):
self.viewModel = viewModel
endLoading()
case .failure(let error):
endLoading(error: error)
}
tableView.reloadData()
if refreshControl.isRefreshing {
refreshControl.endRefreshing()
}
}
}
extension TokensViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.numberOfSections
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let token = viewModel.item(for: indexPath.row, section: indexPath.section)
let cell = TokenViewCell(style: .default, reuseIdentifier: TokenViewCell.identifier)
cell.configure(viewModel: .init(token: token))
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfItems(for: section)
}
}

@ -0,0 +1,37 @@
// Copyright SIX DAY LLC, Inc. All rights reserved.
import Foundation
import UIKit
struct TokensViewModel {
var tokens: [Token] = []
init(tokens: [Token]) {
self.tokens = tokens
}
var title: String {
return "Tokens"
}
var backgroundColor: UIColor {
return .white
}
var hasContent: Bool {
return !tokens.isEmpty
}
var numberOfSections: Int {
return 1
}
func numberOfItems(for section: Int) -> Int {
return tokens.count
}
func item(for row: Int, section: Int) -> Token {
return tokens[row]
}
}

@ -2,10 +2,18 @@
import Foundation
import APIKit
import Result
enum TransactionError: Error {
case failedToFetch
}
enum TokenError: Error {
case failedToFetch
}
protocol TransactionDataStoreDelegate: class {
func didUpdate(viewModel: TransactionsViewModel)
func didFail(with error: Error, viewModel: TransactionsViewModel)
func didUpdate(result: Result<TransactionsViewModel, TransactionError>)
}
class TransactionDataStore {
@ -24,12 +32,11 @@ class TransactionDataStore {
func fetch() {
fetchTransactions()
fetchTokens()
}
func update(transactions: [Transaction]) {
self.transactions = transactions
delegate?.didUpdate(viewModel: viewModel)
delegate?.didUpdate(result: .success(viewModel))
}
func update(tokens: [Token]) {
@ -42,20 +49,8 @@ class TransactionDataStore {
switch result {
case .success(let response):
self.update(transactions: response)
case .failure(let error):
self.delegate?.didFail(with: error, viewModel: self.viewModel)
}
}
}
func fetchTokens() {
let request = GetTokensRequest(address: account.address.address)
Session.send(request) { result in
switch result {
case .success(let response):
self.update(tokens: response)
case .failure(let error):
self.delegate?.didFail(with: error, viewModel: self.viewModel)
case .failure:
self.delegate?.didUpdate(result: .failure(TransactionError.failedToFetch))
}
}
}

@ -4,16 +4,18 @@ import UIKit
import APIKit
import JSONRPCKit
import StatefulViewController
import Result
protocol TransactionsViewControllerDelegate: class {
func didPressSend(for account: Account, in viewController: TransactionsViewController)
func didPressRequest(for account: Account, in viewController: TransactionsViewController)
func didPressTransaction(transaction: Transaction, in viewController: TransactionsViewController)
func didPressTokens(account: Account, in viewController: TransactionsViewController)
}
class TransactionsViewController: UIViewController {
var viewModel: TransactionsViewModel!
var viewModel: TransactionsViewModel = TransactionsViewModel(transactions: [])
private lazy var dataStore: TransactionDataStore = {
return .init(account: self.account)
@ -39,8 +41,6 @@ class TransactionsViewController: UIViewController {
sendButton = Button(size: .extraLarge, style: .squared)
requestButton = Button(size: .extraLarge, style: .squared)
viewModel = TransactionsViewModel(transactions: [])
super.init(nibName: nil, bundle: nil)
dataStore.delegate = self
@ -114,6 +114,12 @@ class TransactionsViewController: UIViewController {
fetch()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
delegate?.didPressTokens(account: account, in: self)
}
func setTitlte(text: String) {
titleView.title = text
}
@ -176,14 +182,19 @@ extension TransactionsViewController: UITableViewDelegate {
}
extension TransactionsViewController: TransactionDataStoreDelegate {
func didFail(with error: Error, viewModel: TransactionsViewModel) {
endLoading(error: error)
func didUpdate(result: Result<TokensViewModel, TokenError>) {
}
func didUpdate(viewModel: TransactionsViewModel) {
self.viewModel = viewModel
func didUpdate(result: Result<TransactionsViewModel, TransactionError>) {
switch result {
case .success(let viewModel):
self.viewModel = viewModel
endLoading()
case .failure(let error):
endLoading(error: error)
}
tableView.reloadData()
endLoading()
if refreshControl.isRefreshing {
refreshControl.endRefreshing()

@ -102,4 +102,19 @@ class AppCoordinatorTests: XCTestCase {
XCTAssertTrue(coordinator.rootNavigationController.viewControllers[0] is TransactionsViewController)
}
func testShowTokens() {
let coordinator = AppCoordinator(
window: UIWindow(),
keystore: FakeKeystore(
accounts: [.make()]
),
rootNavigationController: FakeNavigationController()
)
coordinator.start()
coordinator.showTokens(for: .make())
XCTAssertTrue(coordinator.rootNavigationController.viewControllers[1] is TokensViewController)
}
}

Loading…
Cancel
Save