Skip to content

Commit

Permalink
Fix brave/brave-ios#8349: Transaction Activity v2 UI (brave/brave-ios…
Browse files Browse the repository at this point in the history
…#8390)

* Transaction Activity tab v2 UI

* Resolve `TransactionParserTests`, update `TransactionActivityStoreTests` for date grouping
  • Loading branch information
StephenHeaps committed Nov 16, 2023
1 parent 7bcba90 commit 5510946
Show file tree
Hide file tree
Showing 16 changed files with 1,357 additions and 174 deletions.
5 changes: 5 additions & 0 deletions Sources/BraveUI/Buttons/LoaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private class LoaderLayer: CALayer {
public class LoaderView: UIView {
/// The size of the indicator
public enum Size {
case mini
case small
case normal
case large
Expand All @@ -30,6 +31,8 @@ public class LoaderView: UIView {

fileprivate var size: CGSize {
switch self {
case .mini:
return CGSize(width: 8, height: 8)
case .small:
return CGSize(width: 16, height: 16)
case .normal:
Expand All @@ -41,6 +44,8 @@ public class LoaderView: UIView {

fileprivate var lineWidth: CGFloat {
switch self {
case .mini:
return 1.0
case .small:
return 2.0
case .normal:
Expand Down
170 changes: 152 additions & 18 deletions Sources/BraveWallet/Crypto/AssetIconView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import SwiftUI
import BraveCore
import BraveUI
import DesignSystem

/// Displays an asset's icon from the token registry
///
Expand Down Expand Up @@ -36,26 +37,10 @@ struct AssetIconView: View {
)
}

private var localImage: Image? {
if network.isNativeAsset(token), let uiImage = network.nativeTokenLogoImage {
return Image(uiImage: uiImage)
}

for logo in [token.logo, token.symbol.lowercased()] {
if let baseURL = BraveWallet.TokenRegistryUtils.tokenLogoBaseURL,
case let imageURL = baseURL.appendingPathComponent(logo),
let image = UIImage(contentsOfFile: imageURL.path) {
return Image(uiImage: image)
}
}

return nil
}

var body: some View {
Group {
if let image = localImage {
image
if let uiImage = token.localImage(network: network) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if let url = URL(string: token.logo) {
Expand Down Expand Up @@ -175,3 +160,152 @@ struct NFTIconView: View {
.accessibilityHidden(true)
}
}

/// Displays 2 asset icons stacked on top of one another, with the token's network logo on top.
/// `bottomToken` is displayed in the top-right, `topToken` is displayed in the bottom-left,
/// and the network logo is displayed in the bottom-right.
struct StackedAssetIconsView: View {

var bottomToken: BraveWallet.BlockchainToken?
var topToken: BraveWallet.BlockchainToken?
var network: BraveWallet.NetworkInfo

/// Length of entire view
@ScaledMetric var length: CGFloat = 40
/// Max length of entire view
var maxLength: CGFloat?
@ScaledMetric var networkSymbolLength: CGFloat = 15
var maxNetworkSymbolLength: CGFloat?

/// Size of padding applied to bottom/top icon so they can overlap
private var iconPadding: CGFloat {
length / 5
}

/// Size of asset icon
private var assetIconLength: CGFloat {
length - iconPadding
}

/// Max size of asset icon
private var maxAssetIconLength: CGFloat? {
guard let maxLength else { return nil }
return maxLength - iconPadding
}

var body: some View {
ZStack {
Group {
if let bottomToken {
AssetIconView(
token: bottomToken,
network: network,
shouldShowNetworkIcon: false,
length: assetIconLength,
maxLength: maxAssetIconLength
)
} else { // nil token possible for Solana Swaps
GenericAssetIconView(
backgroundColor: Color(braveSystemName: .gray40),
iconColor: Color.white,
length: assetIconLength,
maxLength: maxAssetIconLength
)
}
}
.padding(.leading, iconPadding)
.padding(.bottom, iconPadding)
.zIndex(0)

Group {
if let topToken {
AssetIconView(
token: topToken,
network: network,
shouldShowNetworkIcon: false,
length: assetIconLength,
maxLength: maxAssetIconLength
)
} else { // nil token possible for Solana Swaps
GenericAssetIconView(
backgroundColor: Color(braveSystemName: .gray20),
iconColor: Color.black,
length: assetIconLength,
maxLength: maxAssetIconLength
)
}
}
.padding(.top, iconPadding)
.padding(.trailing, iconPadding)
.zIndex(1)

if let networkLogoImage = network.networkLogoImage {
Group {
Image(uiImage: networkLogoImage)
.resizable()
.overlay(
Circle()
.stroke(lineWidth: 2)
.foregroundColor(Color(braveSystemName: .containerBackground))
)
.frame(
width: min(networkSymbolLength, maxNetworkSymbolLength ?? networkSymbolLength),
height: min(networkSymbolLength, maxNetworkSymbolLength ?? networkSymbolLength)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.zIndex(2)
}
}
.frame(
width: min(length, maxLength ?? length),
height: min(length, maxLength ?? length)
)
}
}

#if DEBUG
struct StackedAssetIconsView_Previews: PreviewProvider {
static var previews: some View {
StackedAssetIconsView(
bottomToken: nil,
topToken: nil,
network: .mockSolana
)
.previewLayout(.sizeThatFits)
}
}
#endif

struct GenericAssetIconView: View {

let backgroundColor: Color
let iconColor: Color
@ScaledMetric var length: CGFloat
let maxLength: CGFloat?

init(
backgroundColor: Color = Color(braveSystemName: .gray20),
iconColor: Color = Color.black,
length: CGFloat = 40,
maxLength: CGFloat? = nil
) {
self.backgroundColor = backgroundColor
self.iconColor = iconColor
self._length = .init(wrappedValue: length)
self.maxLength = maxLength
}

var body: some View {
Circle()
.fill(backgroundColor)
.frame(width: min(length, maxLength ?? length), height: min(length, maxLength ?? length))
.overlay {
Image(braveSystemName: "leo.crypto.wallets")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(6)
.foregroundColor(iconColor)
}
}
}
74 changes: 48 additions & 26 deletions Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import BraveCore
import SwiftUI

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

@Published private(set) var currencyCode: String = CurrencyCode.usd.code {
/// Sections of transactions for display. Each section represents one date.
@Published var transactionSections: [TransactionSection] = []
/// Filter query to filter the transactions by.
@Published var query: String = ""
/// Selected networks to show transactions for.
@Published var networkFilters: [Selectable<BraveWallet.NetworkInfo>] = [] {
didSet {
currencyFormatter.currencyCode = currencyCode
guard oldValue != currencyCode else { return }
guard !oldValue.isEmpty else { return } // initial assignment to `networkFilters`
update()
}
}
@Published var networkFilters: [Selectable<BraveWallet.NetworkInfo>] = [] {

@Published private(set) var currencyCode: String = CurrencyCode.usd.code {
didSet {
guard !oldValue.isEmpty else { return } // initial assignment to `networkFilters`
currencyFormatter.currencyCode = currencyCode
guard oldValue != currencyCode else { return }
update()
}
}
Expand Down Expand Up @@ -143,7 +147,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
guard !Task.isCancelled else { return }
// display transactions prior to network request to fetch
// estimated solana tx fees & asset prices
self.transactionSummaries = self.transactionSummaries(
self.transactionSections = buildTransactionSections(
transactions: allTransactions,
networksForCoin: networksForCoin,
accountInfos: allAccountInfos,
Expand All @@ -152,7 +156,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
assetRatios: assetPricesCache,
solEstimatedTxFees: solEstimatedTxFeesCache
)
guard !self.transactionSummaries.isEmpty else { return }
guard !self.transactionSections.isEmpty else { return }

if allTransactions.contains(where: { $0.coin == .sol }) {
let solTransactions = allTransactions.filter { $0.coin == .sol }
Expand All @@ -163,7 +167,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
await updateAssetPricesCache(assetRatioIds: allUserAssetsAssetRatioIds)

guard !Task.isCancelled else { return }
self.transactionSummaries = self.transactionSummaries(
self.transactionSections = buildTransactionSections(
transactions: allTransactions,
networksForCoin: networksForCoin,
accountInfos: allAccountInfos,
Expand All @@ -175,30 +179,48 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
}
}

private func transactionSummaries(
private func buildTransactionSections(
transactions: [BraveWallet.TransactionInfo],
networksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]],
accountInfos: [BraveWallet.AccountInfo],
userAssets: [BraveWallet.BlockchainToken],
allTokens: [BraveWallet.BlockchainToken],
assetRatios: [String: Double],
solEstimatedTxFees: [String: UInt64]
) -> [TransactionSummary] {
transactions.compactMap { transaction in
guard let networks = networksForCoin[transaction.coin], let network = networks.first(where: { $0.chainId == transaction.chainId }) else {
return nil
}
return TransactionParser.transactionSummary(
from: transaction,
network: network,
accountInfos: accountInfos,
userAssets: userAssets,
allTokens: allTokens,
assetRatios: assetRatios,
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
currencyFormatter: currencyFormatter
) -> [TransactionSection] {
// Group transactions by day (only compare day/month/year)
let transactionsGroupedByDate = Dictionary(grouping: transactions) { transaction in
let dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: transaction.createdTime)
return Calendar.current.date(from: dateComponents) ?? transaction.createdTime
}
// Map to 1 `TransactionSection` per date
return transactionsGroupedByDate.keys.sorted(by: { $0 > $1 }).compactMap { date in
let transactions = transactionsGroupedByDate[date] ?? []
guard !transactions.isEmpty else { return nil }
let parsedTransactions: [ParsedTransaction] = transactions
.sorted(by: { $0.createdTime > $1.createdTime })
.compactMap { transaction in
guard let networks = networksForCoin[transaction.coin],
let network = networks.first(where: { $0.chainId == transaction.chainId }) else {
return nil
}
return TransactionParser.parseTransaction(
transaction: transaction,
network: network,
accountInfos: accountInfos,
userAssets: userAssets,
allTokens: allTokens,
assetRatios: assetRatios,
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
currencyFormatter: currencyFormatter,
decimalFormatStyle: .decimals(precision: 4)
)
}
return TransactionSection(
date: date,
transactions: parsedTransactions
)
}.sorted(by: { $0.createdTime > $1.createdTime })
}
}

@MainActor private func updateSolEstimatedTxFeesCache(_ solTransactions: [BraveWallet.TransactionInfo]) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ struct SwapTransactionConfirmationView_Previews: PreviewProvider {
fromAddress: BraveWallet.AccountInfo.previewAccount.address,
namedToAddress: "0x Exchange",
toAddress: "0x1111111111222222222233333333334444444444",
networkSymbol: "ETH",
network: .mockMainnet,
details: .ethSwap(.init(
fromToken: .mockUSDCToken,
fromValue: "1.000004",
Expand Down
Loading

0 comments on commit 5510946

Please sign in to comment.