Skip to content

Commit

Permalink
Fix brave/brave-ios#7198: Wallet Activity Tab (brave/brave-ios#7248)
Browse files Browse the repository at this point in the history
* Add Activity tab for displaying user transactions.

* Update `TabbedPageViewController` so the tabs scroll horizontally now; this should resolve any potential truncation issues with new tabs after localization.

* Improve initial load performance by displaying transactions prior to fetching Solana estimated transaction fees and asset prices. 

* Filter out rejected transactions
  • Loading branch information
StephenHeaps authored Apr 19, 2023
1 parent 4427aeb commit 637ee44
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 21 deletions.
8 changes: 8 additions & 0 deletions Sources/BraveWallet/Crypto/CryptoPagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ private class CryptoPagesViewController: TabbedPageViewController {
).then {
$0.title = Strings.Wallet.portfolioPageTitle
},
UIHostingController(
rootView: TransactionsActivityView(
store: cryptoStore.transactionsActivityStore,
networkStore: cryptoStore.networkStore
)
).then {
$0.title = Strings.Wallet.activityPageTitle
},
UIHostingController(
rootView: AccountsView(
cryptoStore: cryptoStore,
Expand Down
10 changes: 10 additions & 0 deletions Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum WebpageRequestResponse: Equatable {
public class CryptoStore: ObservableObject {
public let networkStore: NetworkStore
public let portfolioStore: PortfolioStore
let transactionsActivityStore: TransactionsActivityStore

@Published var buySendSwapDestination: BuySendSwapDestination? {
didSet {
Expand Down Expand Up @@ -129,6 +130,15 @@ public class CryptoStore: ObservableObject {
blockchainRegistry: blockchainRegistry,
ipfsApi: ipfsApi
)
self.transactionsActivityStore = .init(
keyringService: keyringService,
rpcService: rpcService,
walletService: walletService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
txService: txService,
solTxManagerProxy: solTxManagerProxy
)

self.keyringService.add(self)
self.txService.add(self)
Expand Down
12 changes: 1 addition & 11 deletions Sources/BraveWallet/Crypto/Stores/TabbedPageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,7 @@ private class TabsBarView: UIView, UICollectionViewDelegate {
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
flowLayout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
// When we add back more items to the pages list, switch this back to estimated and remove
// item size setting within `layoutSubviews`
flowLayout.itemSize = CGSize(width: 44, height: tabBarHeight)
flowLayout.estimatedItemSize = CGSize(width: 44, height: tabBarHeight)
return flowLayout
}()
)
Expand All @@ -364,14 +362,6 @@ private class TabsBarView: UIView, UICollectionViewDelegate {
}
}

override func layoutSubviews() {
super.layoutSubviews()

guard bounds.width != 0 && collectionView.numberOfItems(inSection: 0) != 0 else { return }
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?
.itemSize = CGSize(width: (bounds.width - 32) / CGFloat(collectionView.numberOfItems(inSection: 0)), height: tabBarHeight)
}

override init(frame: CGRect) {
super.init(frame: frame)

Expand Down
217 changes: 217 additions & 0 deletions Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/* Copyright 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import BraveCore
import SwiftUI

class TransactionsActivityStore: ObservableObject {
@Published var transactionSummaries: [TransactionSummary] = []

@Published private(set) var currencyCode: String = CurrencyCode.usd.code {
didSet {
currencyFormatter.currencyCode = currencyCode
guard oldValue != currencyCode else { return }
update()
}
}

let currencyFormatter: NumberFormatter = .usdCurrencyFormatter

private var solEstimatedTxFeesCache: [String: UInt64] = [:]
private var assetPricesCache: [String: Double] = [:]

private let keyringService: BraveWalletKeyringService
private let rpcService: BraveWalletJsonRpcService
private let walletService: BraveWalletBraveWalletService
private let assetRatioService: BraveWalletAssetRatioService
private let blockchainRegistry: BraveWalletBlockchainRegistry
private let txService: BraveWalletTxService
private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy

init(
keyringService: BraveWalletKeyringService,
rpcService: BraveWalletJsonRpcService,
walletService: BraveWalletBraveWalletService,
assetRatioService: BraveWalletAssetRatioService,
blockchainRegistry: BraveWalletBlockchainRegistry,
txService: BraveWalletTxService,
solTxManagerProxy: BraveWalletSolanaTxManagerProxy
) {
self.keyringService = keyringService
self.rpcService = rpcService
self.walletService = walletService
self.assetRatioService = assetRatioService
self.blockchainRegistry = blockchainRegistry
self.txService = txService
self.solTxManagerProxy = solTxManagerProxy

keyringService.add(self)
txService.add(self)

Task { @MainActor in
self.currencyCode = await walletService.defaultBaseCurrency()
}
}

private var updateTask: Task<Void, Never>?
func update() {
updateTask?.cancel()
updateTask = Task { @MainActor in
let allKeyrings = await keyringService.keyrings(
for: WalletConstants.supportedCoinTypes
)
let allAccountInfos = allKeyrings.flatMap(\.accountInfos)
// Only transactions for the selected network
// for each coin type are returned
var selectedNetworkForCoin: [BraveWallet.CoinType: BraveWallet.NetworkInfo] = [:]
for coin in WalletConstants.supportedCoinTypes {
selectedNetworkForCoin[coin] = await rpcService.network(coin)
}
let allTransactions = await txService.allTransactions(
for: allKeyrings
).filter { $0.txStatus != .rejected }
let userVisibleTokens = await walletService.allVisibleUserAssets(
in: Array(selectedNetworkForCoin.values)
).flatMap(\.tokens)
let allTokens = await blockchainRegistry.allTokens(
in: Array(selectedNetworkForCoin.values)
).flatMap(\.tokens)
guard !Task.isCancelled else { return }
// display transactions prior to network request to fetch
// estimated solana tx fees & asset prices
self.transactionSummaries = self.transactionSummaries(
transactions: allTransactions,
selectedNetworkForCoin: selectedNetworkForCoin,
accountInfos: allAccountInfos,
userVisibleTokens: userVisibleTokens,
allTokens: allTokens,
assetRatios: assetPricesCache,
solEstimatedTxFees: solEstimatedTxFeesCache
)
guard !self.transactionSummaries.isEmpty else { return }

if allTransactions.contains(where: { $0.coin == .sol }) {
let solTransactionIds = allTransactions.filter { $0.coin == .sol }.map(\.id)
await updateSolEstimatedTxFeesCache(solTransactionIds: solTransactionIds)
}

let allVisibleTokenAssetRatioIds = userVisibleTokens.map(\.assetRatioId)
await updateAssetPricesCache(assetRatioIds: allVisibleTokenAssetRatioIds)

guard !Task.isCancelled else { return }
self.transactionSummaries = self.transactionSummaries(
transactions: allTransactions,
selectedNetworkForCoin: selectedNetworkForCoin,
accountInfos: allAccountInfos,
userVisibleTokens: userVisibleTokens,
allTokens: allTokens,
assetRatios: assetPricesCache,
solEstimatedTxFees: solEstimatedTxFeesCache
)
}
}

private func transactionSummaries(
transactions: [BraveWallet.TransactionInfo],
selectedNetworkForCoin: [BraveWallet.CoinType: BraveWallet.NetworkInfo],
accountInfos: [BraveWallet.AccountInfo],
userVisibleTokens: [BraveWallet.BlockchainToken],
allTokens: [BraveWallet.BlockchainToken],
assetRatios: [String: Double],
solEstimatedTxFees: [String: UInt64]
) -> [TransactionSummary] {
transactions.compactMap { transaction in
guard let network = selectedNetworkForCoin[transaction.coin] else {
return nil
}
return TransactionParser.transactionSummary(
from: transaction,
network: network,
accountInfos: accountInfos,
visibleTokens: userVisibleTokens,
allTokens: allTokens,
assetRatios: assetRatios,
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
currencyFormatter: currencyFormatter
)
}.sorted(by: { $0.createdTime > $1.createdTime })
}

@MainActor private func updateSolEstimatedTxFeesCache(solTransactionIds: [String]) async {
let fees = await solTxManagerProxy.estimatedTxFees(for: solTransactionIds)
for (key, value) in fees { // update cached values
self.solEstimatedTxFeesCache[key] = value
}
}

@MainActor private func updateAssetPricesCache(assetRatioIds: [String]) async {
let prices = await assetRatioService.fetchPrices(
for: assetRatioIds,
toAssets: [currencyFormatter.currencyCode],
timeframe: .oneDay
).compactMapValues { Double($0) }
for (key, value) in prices { // update cached values
self.assetPricesCache[key] = value
}
}

func transactionDetailsStore(
for transaction: BraveWallet.TransactionInfo
) -> TransactionDetailsStore {
TransactionDetailsStore(
transaction: transaction,
keyringService: keyringService,
walletService: walletService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
solanaTxManagerProxy: solTxManagerProxy
)
}
}

extension TransactionsActivityStore: BraveWalletKeyringServiceObserver {
func keyringCreated(_ keyringId: String) { }

func keyringRestored(_ keyringId: String) { }

func keyringReset() { }

func locked() { }

func unlocked() { }

func backedUp() { }

func accountsChanged() {
update()
}

func accountsAdded(_ coin: BraveWallet.CoinType, addresses: [String]) {
update()
}

func autoLockMinutesChanged() { }

func selectedAccountChanged(_ coin: BraveWallet.CoinType) { }
}

extension TransactionsActivityStore: BraveWalletTxServiceObserver {
func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) {
update()
}

func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) {
update()
}

func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) {
update()
}

func onTxServiceReset() {
update()
}
}
77 changes: 77 additions & 0 deletions Sources/BraveWallet/Crypto/TransactionsActivityView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* Copyright 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import BraveCore
import SwiftUI

struct TransactionsActivityView: View {

@ObservedObject var store: TransactionsActivityStore
@ObservedObject var networkStore: NetworkStore

@State private var isPresentingNetworkFilter = false
@State private var transactionDetails: TransactionDetailsStore?

var body: some View {
List {
Section {
if store.transactionSummaries.isEmpty {
emptyState
.listRowBackground(Color.clear)
} else {
ForEach(store.transactionSummaries) { txSummary in
Button(action: {
self.transactionDetails = store.transactionDetailsStore(for: txSummary.txInfo)
}) {
TransactionSummaryView(summary: txSummary)
}
}
}
}
}
.onAppear {
store.update()
}
.sheet(
isPresented: Binding(
get: { self.transactionDetails != nil },
set: { if !$0 { self.transactionDetails = nil } }
)
) {
if let transactionDetailsStore = self.transactionDetails {
TransactionDetailsView(
transactionDetailsStore: transactionDetailsStore,
networkStore: networkStore
)
}
}
}

private var emptyState: some View {
VStack(alignment: .center, spacing: 10) {
Text(Strings.Wallet.activityPageEmptyTitle)
.font(.headline.weight(.semibold))
.foregroundColor(Color(.braveLabel))
Text(Strings.Wallet.activityPageEmptyDescription)
.font(.subheadline.weight(.semibold))
.foregroundColor(Color(.secondaryLabel))
}
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(.vertical, 60)
.padding(.horizontal, 32)
}
}

#if DEBUG
struct TransactionsActivityViewView_Previews: PreviewProvider {
static var previews: some View {
TransactionsActivityView(
store: .preview,
networkStore: .previewStore
)
}
}
#endif
Loading

0 comments on commit 637ee44

Please sign in to comment.