From bde11ad2b95350ccb5fdd5948a33f5425ca8667b Mon Sep 17 00:00:00 2001
From: StephenHeaps <5314553+StephenHeaps@users.noreply.github.com>
Date: Wed, 29 Nov 2023 11:40:35 -0500
Subject: [PATCH] Fix #8379: Transaction Details v2 UI (#8474)
* Transaction Details v2 UI
---
.../tx-details-lines.imageset/Contents.json | 21 +
.../tx_details_lines.svg | 1 +
.../Activity/AccountActivityView.swift | 7 +-
.../Asset Details/AssetDetailView.swift | 7 +-
Sources/BraveWallet/Crypto/NFT/NFTView.swift | 4 +-
.../Crypto/Portfolio/AssetButton.swift | 29 -
.../Portfolio/PortfolioAssetsView.swift | 4 +-
.../Crypto/Portfolio/WalletIconButton.swift | 61 ++
.../Crypto/Stores/AccountActivityStore.swift | 14 +-
.../Crypto/Stores/AssetDetailStore.swift | 19 +-
.../Crypto/Stores/CryptoStore.swift | 2 +
.../Stores/TransactionConfirmationStore.swift | 23 +-
.../Stores/TransactionDetailsStore.swift | 134 ++--
.../Stores/TransactionsActivityStore.swift | 38 +-
.../TransactionConfirmationView.swift | 7 +-
.../Transactions/TransactionDetailsView.swift | 631 +++++++++++++++---
.../Transactions/TransactionsListView.swift | 2 +-
.../Crypto/TransactionsActivityView.swift | 7 +-
.../Extensions/BraveWalletExtensions.swift | 31 +
.../Preview Content/MockStores.swift | 2 +
.../WalletHostingViewController.swift | 2 +-
Sources/BraveWallet/WalletStrings.swift | 2 +-
.../leo.arrow.right.symbolset/Contents.json | 11 +
.../leo.copy.symbolset/Contents.json | 11 +
.../Contents.json | 11 +
.../AssetDetailStoreTests.swift | 3 +
.../TransactionConfirmationStoreTests.swift | 1 +
27 files changed, 883 insertions(+), 202 deletions(-)
create mode 100644 Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/Contents.json
create mode 100644 Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/tx_details_lines.svg
delete mode 100644 Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift
create mode 100644 Sources/BraveWallet/Crypto/Portfolio/WalletIconButton.swift
create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.arrow.right.symbolset/Contents.json
create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.copy.symbolset/Contents.json
create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.warning.circle-outline.symbolset/Contents.json
diff --git a/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/Contents.json b/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/Contents.json
new file mode 100644
index 00000000000..403ccc40b83
--- /dev/null
+++ b/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tx_details_lines.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/tx_details_lines.svg b/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/tx_details_lines.svg
new file mode 100644
index 00000000000..38921d35049
--- /dev/null
+++ b/Sources/BraveWallet/Assets.xcassets/Brave Wallet/Transaction States/tx-details-lines.imageset/tx_details_lines.svg
@@ -0,0 +1 @@
+
diff --git a/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift b/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift
index 1cd5f303eae..851313250bd 100644
--- a/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift
+++ b/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift
@@ -153,7 +153,12 @@ struct AccountActivityView: View {
.sheet(
isPresented: Binding(
get: { self.transactionDetails != nil },
- set: { if !$0 { self.transactionDetails = nil } }
+ set: {
+ if !$0 {
+ self.transactionDetails = nil
+ self.activityStore.closeTransactionDetailsStore()
+ }
+ }
)
) {
if let transactionDetailsStore = transactionDetails {
diff --git a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift
index 845877179a7..58a0088d43e 100644
--- a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift
+++ b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift
@@ -237,7 +237,12 @@ struct AssetDetailView: View {
.sheet(
isPresented: Binding(
get: { self.transactionDetails != nil },
- set: { if !$0 { self.transactionDetails = nil } }
+ set: {
+ if !$0 {
+ self.transactionDetails = nil
+ self.assetDetailStore.closeTransactionDetailsStore()
+ }
+ }
)
) {
if let transactionDetailsStore = transactionDetails {
diff --git a/Sources/BraveWallet/Crypto/NFT/NFTView.swift b/Sources/BraveWallet/Crypto/NFT/NFTView.swift
index 0bd435495e6..1d03c1a22ec 100644
--- a/Sources/BraveWallet/Crypto/NFT/NFTView.swift
+++ b/Sources/BraveWallet/Crypto/NFT/NFTView.swift
@@ -98,7 +98,7 @@ struct NFTView: View {
}
private var filtersButton: some View {
- AssetButton(braveSystemName: "leo.filter.settings", action: {
+ WalletIconButton(braveSystemName: "leo.filter.settings", action: {
isPresentingFiltersDisplaySettings = true
})
}
@@ -150,7 +150,7 @@ struct NFTView: View {
}
private var addCustomAssetButton: some View {
- AssetButton(braveSystemName: "leo.plus.add") {
+ WalletIconButton(braveSystemName: "leo.plus.add") {
isShowingAddCustomNFT = true
}
}
diff --git a/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift b/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift
deleted file mode 100644
index 4fa0522fca1..00000000000
--- a/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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 SwiftUI
-import DesignSystem
-
-struct AssetButton: View {
-
- let braveSystemName: String
- let action: () -> Void
-
- @ScaledMetric var length = 36
-
- var body: some View {
- Button(action: action) {
- Image(braveSystemName: braveSystemName)
- .foregroundColor(Color(braveSystemName: .iconInteractive))
- .imageScale(.medium)
- .padding(6)
- .frame(width: length, height: length)
- .background(
- Circle()
- .strokeBorder(Color(braveSystemName: .dividerInteractive), lineWidth: 1)
- )
- }
- }
-}
diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift
index 967fff09487..a97945befd7 100644
--- a/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift
+++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift
@@ -83,7 +83,7 @@ struct PortfolioAssetsView: View {
}
private var editUserAssetsButton: some View {
- AssetButton(braveSystemName: "leo.list.settings", action: {
+ WalletIconButton(braveSystemName: "leo.list.settings", action: {
isPresentingEditUserAssets = true
})
.sheet(isPresented: $isPresentingEditUserAssets) {
@@ -98,7 +98,7 @@ struct PortfolioAssetsView: View {
}
private var filtersButton: some View {
- AssetButton(braveSystemName: "leo.filter.settings", action: {
+ WalletIconButton(braveSystemName: "leo.filter.settings", action: {
isPresentingFiltersDisplaySettings = true
})
.sheet(isPresented: $isPresentingFiltersDisplaySettings) {
diff --git a/Sources/BraveWallet/Crypto/Portfolio/WalletIconButton.swift b/Sources/BraveWallet/Crypto/Portfolio/WalletIconButton.swift
new file mode 100644
index 00000000000..d2d4c05a47e
--- /dev/null
+++ b/Sources/BraveWallet/Crypto/Portfolio/WalletIconButton.swift
@@ -0,0 +1,61 @@
+/* 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 SwiftUI
+import DesignSystem
+
+struct WalletIconButton: View {
+
+ enum IconSymbol {
+ case braveSystemName(String)
+ case systemName(String)
+ }
+
+ let iconSymbol: IconSymbol
+ let action: () -> Void
+
+ @ScaledMetric var length: CGFloat = 36
+
+ init(
+ braveSystemName: String,
+ action: @escaping () -> Void,
+ length: CGFloat = 36
+ ) {
+ self.iconSymbol = .braveSystemName(braveSystemName)
+ self.action = action
+ self._length = .init(wrappedValue: length)
+ }
+
+ init(
+ systemName: String,
+ action: @escaping () -> Void,
+ length: CGFloat = 36
+ ) {
+ self.iconSymbol = .systemName(systemName)
+ self.action = action
+ self._length = .init(wrappedValue: length)
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Group {
+ switch iconSymbol {
+ case .braveSystemName(let braveSystemName):
+ Image(braveSystemName: braveSystemName)
+ case .systemName(let systemName):
+ Image(systemName: systemName)
+ }
+ }
+ .foregroundColor(Color(braveSystemName: .iconInteractive))
+ .imageScale(.medium)
+ .padding(6)
+ .frame(width: length, height: length)
+ .background(
+ Circle()
+ .strokeBorder(Color(braveSystemName: .dividerInteractive), lineWidth: 1)
+ )
+ }
+ }
+}
diff --git a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift
index d282a9a3d9f..b7137e35961 100644
--- a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift
@@ -84,6 +84,7 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
rpcServiceObserver = nil
txServiceObserver = nil
walletServiceObserver = nil
+ transactionDetailsStore?.tearDown()
}
func setupObservers() {
@@ -311,17 +312,28 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
}.sorted(by: { $0.createdTime > $1.createdTime })
}
+ private var transactionDetailsStore: TransactionDetailsStore?
func transactionDetailsStore(for transaction: BraveWallet.TransactionInfo) -> TransactionDetailsStore {
- TransactionDetailsStore(
+ let transactionDetailsStore = TransactionDetailsStore(
transaction: transaction,
+ parsedTransaction: nil,
keyringService: keyringService,
walletService: walletService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
+ txService: txService,
solanaTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
userAssetManager: assetManager
)
+ self.transactionDetailsStore = transactionDetailsStore
+ return transactionDetailsStore
+ }
+
+ func closeTransactionDetailsStore() {
+ self.transactionDetailsStore?.tearDown()
+ self.transactionDetailsStore = nil
}
#if DEBUG
diff --git a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
index 8a902bb38a8..5813d388dcb 100644
--- a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
@@ -76,6 +76,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
private let txService: BraveWalletTxService
private let blockchainRegistry: BraveWalletBlockchainRegistry
private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy
+ private let ipfsApi: IpfsAPI
private let swapService: BraveWalletSwapService
private let assetManager: WalletUserAssetManagerType
/// A list of tokens that are supported with the current selected network for all supported
@@ -119,6 +120,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
txService: BraveWalletTxService,
blockchainRegistry: BraveWalletBlockchainRegistry,
solTxManagerProxy: BraveWalletSolanaTxManagerProxy,
+ ipfsApi: IpfsAPI,
swapService: BraveWalletSwapService,
userAssetManager: WalletUserAssetManagerType,
assetDetailType: AssetDetailType
@@ -130,6 +132,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
self.txService = txService
self.blockchainRegistry = blockchainRegistry
self.solTxManagerProxy = solTxManagerProxy
+ self.ipfsApi = ipfsApi
self.swapService = swapService
self.assetManager = userAssetManager
self.assetDetailType = assetDetailType
@@ -145,6 +148,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
keyringServiceObserver = nil
txServiceObserver = nil
walletServiceObserver = nil
+ transactionDetailsStore?.tearDown()
}
func setupObservers() {
@@ -383,17 +387,28 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
}
}
+ private var transactionDetailsStore: TransactionDetailsStore?
func transactionDetailsStore(for transaction: BraveWallet.TransactionInfo) -> TransactionDetailsStore {
- TransactionDetailsStore(
+ let transactionDetailsStore = TransactionDetailsStore(
transaction: transaction,
+ parsedTransaction: nil,
keyringService: keyringService,
walletService: walletService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
+ txService: txService,
solanaTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
userAssetManager: assetManager
)
+ self.transactionDetailsStore = transactionDetailsStore
+ return transactionDetailsStore
+ }
+
+ func closeTransactionDetailsStore() {
+ self.transactionDetailsStore?.tearDown()
+ self.transactionDetailsStore = nil
}
/// Should be called after dismissing create account. Returns true if an account was created
@@ -452,7 +467,7 @@ extension AssetDetailStore: BraveWalletTxServiceObserver {
update()
}
func onTxServiceReset() {
- }
+ }
}
extension AssetDetailStore: BraveWalletBraveWalletServiceObserver {
diff --git a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
index 66cb760db06..a70fe3d6e33 100644
--- a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
@@ -436,6 +436,7 @@ public class CryptoStore: ObservableObject, WalletObserverStore {
txService: txService,
blockchainRegistry: blockchainRegistry,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
swapService: swapService,
userAssetManager: userAssetManager,
assetDetailType: assetDetailType
@@ -499,6 +500,7 @@ public class CryptoStore: ObservableObject, WalletObserverStore {
ethTxManagerProxy: ethTxManagerProxy,
keyringService: keyringService,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
userAssetManager: userAssetManager
)
confirmationStore = store
diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift
index e3ae22adcf0..1bc840338ae 100644
--- a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift
@@ -138,6 +138,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
private let ethTxManagerProxy: BraveWalletEthTxManagerProxy
private let keyringService: BraveWalletKeyringService
private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy
+ private let ipfsApi: IpfsAPI
private let assetManager: WalletUserAssetManagerType
private var selectedChain: BraveWallet.NetworkInfo = .init()
private var txServiceObserver: TxServiceObserver?
@@ -156,6 +157,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
ethTxManagerProxy: BraveWalletEthTxManagerProxy,
keyringService: BraveWalletKeyringService,
solTxManagerProxy: BraveWalletSolanaTxManagerProxy,
+ ipfsApi: IpfsAPI,
userAssetManager: WalletUserAssetManagerType
) {
self.assetRatioService = assetRatioService
@@ -166,6 +168,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
self.ethTxManagerProxy = ethTxManagerProxy
self.keyringService = keyringService
self.solTxManagerProxy = solTxManagerProxy
+ self.ipfsApi = ipfsApi
self.assetManager = userAssetManager
self.setupObservers()
@@ -178,6 +181,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
func tearDown() {
txServiceObserver = nil
walletServiceObserver = nil
+ txDetailsStore?.tearDown()
}
func setupObservers() {
@@ -337,18 +341,35 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
}
}
+ private var txDetailsStore: TransactionDetailsStore?
func activeTxDetailsStore() -> TransactionDetailsStore {
let tx = allTxs.first { $0.id == activeTransactionId } ?? activeParsedTransaction.transaction
- return TransactionDetailsStore(
+ let parsedTransaction: ParsedTransaction?
+ if activeParsedTransaction.transaction.id == tx.id {
+ parsedTransaction = activeParsedTransaction
+ } else {
+ parsedTransaction = nil
+ }
+ let txDetailsStore = TransactionDetailsStore(
transaction: tx,
+ parsedTransaction: parsedTransaction,
keyringService: keyringService,
walletService: walletService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
+ txService: txService,
solanaTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
userAssetManager: assetManager
)
+ self.txDetailsStore = txDetailsStore
+ return txDetailsStore
+ }
+
+ func closeTxDetailsStore() {
+ self.txDetailsStore?.tearDown()
+ self.txDetailsStore = nil
}
private func clearTrasactionInfoBeforeUpdate() {
diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift
index af9b386f1d9..e6e3565469a 100644
--- a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift
@@ -11,15 +11,11 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
let transaction: BraveWallet.TransactionInfo
@Published private(set) var parsedTransaction: ParsedTransaction?
@Published private(set) var network: BraveWallet.NetworkInfo?
- @Published private(set) var title: String?
- @Published private(set) var value: String?
- @Published private(set) var fiat: String?
- @Published private(set) var gasFee: String?
- @Published private(set) var marketPrice: String?
@Published private(set) var currencyCode: String = CurrencyCode.usd.code {
didSet {
currencyFormatter.currencyCode = currencyCode
+ guard currencyCode != oldValue else { return }
update()
}
}
@@ -34,38 +30,76 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
private let rpcService: BraveWalletJsonRpcService
private let assetRatioService: BraveWalletAssetRatioService
private let blockchainRegistry: BraveWalletBlockchainRegistry
+ private let txService: BraveWalletTxService
private let solanaTxManagerProxy: BraveWalletSolanaTxManagerProxy
+ private let ipfsApi: IpfsAPI
private let assetManager: WalletUserAssetManagerType
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:]
+ private var nftMetadataCache: [String: NFTMetadata] = [:]
- var isObserving: Bool = false
+ var isObserving: Bool {
+ txServiceObserver != nil
+ }
+
+ private var txServiceObserver: TxServiceObserver?
init(
transaction: BraveWallet.TransactionInfo,
+ parsedTransaction: ParsedTransaction?,
keyringService: BraveWalletKeyringService,
walletService: BraveWalletBraveWalletService,
rpcService: BraveWalletJsonRpcService,
assetRatioService: BraveWalletAssetRatioService,
blockchainRegistry: BraveWalletBlockchainRegistry,
+ txService: BraveWalletTxService,
solanaTxManagerProxy: BraveWalletSolanaTxManagerProxy,
+ ipfsApi: IpfsAPI,
userAssetManager: WalletUserAssetManagerType
) {
self.transaction = transaction
+ self.parsedTransaction = parsedTransaction
self.keyringService = keyringService
self.walletService = walletService
self.rpcService = rpcService
self.assetRatioService = assetRatioService
self.blockchainRegistry = blockchainRegistry
+ self.txService = txService
self.solanaTxManagerProxy = solanaTxManagerProxy
+ self.ipfsApi = ipfsApi
self.assetManager = userAssetManager
+ setupObservers()
+
walletService.defaultBaseCurrency { [self] currencyCode in
self.currencyCode = currencyCode
}
}
+ func setupObservers() {
+ guard !isObserving else { return }
+ self.txServiceObserver = TxServiceObserver(
+ txService: txService,
+ _onNewUnapprovedTx: { [weak self] _ in
+ self?.update()
+ },
+ _onUnapprovedTxUpdated: { [weak self] _ in
+ self?.update()
+ },
+ _onTransactionStatusChanged: { [weak self] _ in
+ self?.update()
+ },
+ _onTxServiceReset: { [weak self] in
+ self?.update()
+ }
+ )
+ }
+
+ func tearDown() {
+ txServiceObserver = nil
+ }
+
func update() {
Task { @MainActor in
let coin = transaction.coin
@@ -112,77 +146,49 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
userAssets: userAssets,
allTokens: allTokens,
assetRatios: assetRatios,
- nftMetadata: [:],
+ nftMetadata: nftMetadataCache,
solEstimatedTxFee: solEstimatedTxFee,
currencyFormatter: currencyFormatter
) else {
return
}
self.parsedTransaction = parsedTransaction
- self.currencyFormatter.maximumFractionDigits = 2 // use max. 2 digits for market price calculation
-
+
+ // Fetch NFTMetadata if needed.
+ let nftToken: BraveWallet.BlockchainToken?
switch parsedTransaction.details {
- case let .ethSend(details),
- let .erc20Transfer(details),
- let .solSystemTransfer(details),
- let .solSplTokenTransfer(details):
- self.title = Strings.Wallet.sent
- self.value = String(format: "%@ %@", details.fromAmount, details.fromToken?.symbol ?? "")
- self.fiat = details.fromFiat
- if let fromToken = details.fromToken, let tokenPrice = assetRatios[fromToken.assetRatioId.lowercased()] {
- self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00"
- }
- case let .ethSwap(details):
- self.title = Strings.Wallet.swap
- if let fromToken = details.fromToken {
- self.value = String(format: "%@ %@", details.fromAmount, fromToken.symbol)
- if let tokenPrice = assetRatios[fromToken.assetRatioId.lowercased()] {
- self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00"
- }
+ case .erc721Transfer(let details):
+ if details.nftMetadata == nil {
+ nftToken = details.fromToken
} else {
- self.value = details.fromAmount
- }
- self.fiat = details.fromFiat
- case let .ethErc20Approve(details):
- var token = details.token
- if token == nil {
- token = await self.fetchTokenInfo(for: details.tokenContractAddress)
- }
-
- self.title = Strings.Wallet.approveNetworkButtonTitle
- self.value = String(format: "%@ %@", details.approvalAmount, token?.symbol ?? "")
- if let token = token, let tokenPrice = assetRatios[token.assetRatioId.lowercased()] {
- self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00"
+ nftToken = nil
}
- case let .erc721Transfer(details):
- self.title = Strings.Wallet.swap
- if let fromToken = details.fromToken {
- self.value = String(format: "%@ %@", details.fromAmount, fromToken.symbol)
- if let tokenPrice = assetRatios[fromToken.assetRatioId.lowercased()] {
- self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00"
- }
+ case .solSplTokenTransfer(let details):
+ if let fromToken = details.fromToken,
+ fromToken.isNft,
+ details.fromTokenMetadata == nil {
+ nftToken = fromToken
} else {
- self.value = details.fromAmount
+ nftToken = nil
}
- case let .solDappTransaction(details):
- self.title = Strings.Wallet.solanaDappTransactionTitle
- self.value = details.fromAmount
- case let .solSwapTransaction(details):
- self.title = Strings.Wallet.solanaSwapTransactionTitle
- self.value = details.fromAmount
- case let .filSend(details):
- self.title = Strings.Wallet.sent
- self.value = String(format: "%@ %@", details.sendAmount, details.sendToken?.symbol ?? "")
- self.fiat = details.sendFiat
- if let sendToken = details.sendToken, let tokenPrice = assetRatios[sendToken.assetRatioId.lowercased()] {
- self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00"
- }
- case .other:
- break
+ default:
+ nftToken = nil
}
- if let gasFee = parsedTransaction.gasFee {
- self.gasFee = String(format: "%@ %@\n%@", gasFee.fee, parsedTransaction.networkSymbol, gasFee.fiat)
+ guard let nftToken else { return }
+ self.nftMetadataCache[nftToken.id] = await rpcService.fetchNFTMetadata(for: nftToken, ipfsApi: ipfsApi)
+ guard let parsedTransaction = transaction.parsedTransaction(
+ network: network,
+ accountInfos: allAccounts,
+ userAssets: userAssets,
+ allTokens: allTokens,
+ assetRatios: assetRatios,
+ nftMetadata: nftMetadataCache,
+ solEstimatedTxFee: solEstimatedTxFee,
+ currencyFormatter: currencyFormatter
+ ) else {
+ return
}
+ self.parsedTransaction = parsedTransaction
}
}
diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
index 981fe401208..7fbb1277b80 100644
--- a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
+++ b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
@@ -5,6 +5,7 @@
import BraveCore
import SwiftUI
+import Preferences
class TransactionsActivityStore: ObservableObject, WalletObserverStore {
/// Sections of transactions for display. Each section represents one date.
@@ -73,7 +74,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
self.assetManager = userAssetManager
self.setupObservers()
-
+ Preferences.Wallet.showTestNetworks.observe(from: self)
Task { @MainActor in
self.currencyCode = await walletService.defaultBaseCurrency()
}
@@ -83,6 +84,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
keyringServiceObserver = nil
txServiceObserver = nil
walletServiceObserver = nil
+ transactionDetailsStore?.tearDown()
}
func setupObservers() {
@@ -285,18 +287,50 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
}
}
+ private var transactionDetailsStore: TransactionDetailsStore?
func transactionDetailsStore(
for transaction: BraveWallet.TransactionInfo
) -> TransactionDetailsStore {
- TransactionDetailsStore(
+ let parsedTransaction = transactionSections
+ .flatMap(\.transactions)
+ .first(where: { $0.transaction.id == transaction.id })
+ let transactionDetailsStore = TransactionDetailsStore(
transaction: transaction,
+ parsedTransaction: parsedTransaction,
keyringService: keyringService,
walletService: walletService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
+ txService: txService,
solanaTxManagerProxy: solTxManagerProxy,
+ ipfsApi: ipfsApi,
userAssetManager: assetManager
)
+ self.transactionDetailsStore = transactionDetailsStore
+ return transactionDetailsStore
+ }
+
+ func closeTransactionDetailsStore() {
+ self.transactionDetailsStore?.tearDown()
+ self.transactionDetailsStore = nil
+ }
+}
+
+extension TransactionsActivityStore: PreferencesObserver {
+ public func preferencesDidChange(for key: String) {
+ guard key == Preferences.Wallet.showTestNetworks.key else { return }
+ Task { @MainActor in
+ let allNetworks = await self.rpcService.allNetworksForSupportedCoins()
+ self.networkFilters = allNetworks.map { network in
+ // if user previously de-selected a network, keep it de-selected
+ let isSelected: Bool = self.networkFilters
+ .first(where: { selectedNetworkModel in
+ selectedNetworkModel.model.chainId == network.chainId
+ && selectedNetworkModel.model.coin == network.coin
+ })?.isSelected ?? true
+ return .init(isSelected: isSelected, model: network)
+ }
+ }
}
}
diff --git a/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionConfirmationView.swift b/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionConfirmationView.swift
index a97c6b177f9..2d9413050d2 100644
--- a/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionConfirmationView.swift
+++ b/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionConfirmationView.swift
@@ -144,7 +144,12 @@ struct TransactionConfirmationView: View {
.sheet(
isPresented: Binding(
get: { self.transactionDetails != nil },
- set: { if !$0 { self.transactionDetails = nil } }
+ set: {
+ if !$0 {
+ self.transactionDetails = nil
+ self.confirmationStore.closeTxDetailsStore()
+ }
+ }
)
) {
if let transactionDetailsStore = transactionDetails {
diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift
index 825d5bc4a15..23daeadb1ff 100644
--- a/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift
+++ b/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift
@@ -14,122 +14,569 @@ struct TransactionDetailsView: View {
@ObservedObject var transactionDetailsStore: TransactionDetailsStore
@ObservedObject var networkStore: NetworkStore
-
- @Environment(\.presentationMode) @Binding private var presentationMode
+
@Environment(\.openURL) private var openWalletURL
- init(
- transactionDetailsStore: TransactionDetailsStore,
- networkStore: NetworkStore
- ) {
- self.transactionDetailsStore = transactionDetailsStore
- self.networkStore = networkStore
+ var body: some View {
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ Text(Strings.Wallet.transactionDetailsTitle)
+ .font(.title2.weight(.medium))
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ DetailsView(
+ transaction: transactionDetailsStore.transaction,
+ parsedTransaction: transactionDetailsStore.parsedTransaction
+ )
+ .padding(.bottom, 24 - 16)
+
+ if let parsedTransaction = transactionDetailsStore.parsedTransaction {
+ if !parsedTransaction.transaction.txHash.isEmpty {
+ HStack {
+ VStack(alignment: .leading) {
+ rowTitle(Strings.Wallet.transactionDetailsTxHashTitle)
+ Text(parsedTransaction.transaction.txHash.zwspOutput) // zwspOutput to avoid hyphen when wrapped
+ .font(.callout)
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ }
+ Spacer()
+ WalletIconButton(braveSystemName: "leo.copy") {
+ UIPasteboard.general.string = parsedTransaction.transaction.txHash
+ }
+ WalletIconButton(systemName: "arrow.up.forward.square") {
+ if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == transactionDetailsStore.transaction.chainId }),
+ let url = txNetwork.txBlockExplorerLink(txHash: transactionDetailsStore.transaction.txHash, for: txNetwork.coin) {
+ openWalletURL(url)
+ }
+ }
+ }
+ }
+
+ Divider()
+
+ HStack {
+ VStack(alignment: .leading) {
+ rowTitle(Strings.Wallet.swapCryptoFromTitle)
+ AddressView(address: parsedTransaction.fromAddress) {
+ Text(parsedTransaction.fromAddress.zwspOutput) // zwspOutput to avoid hyphen when wrapped
+ .font(.callout)
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ if isLocalAccount(
+ address: parsedTransaction.fromAddress,
+ namedAddress: parsedTransaction.namedFromAddress
+ ) { // only show named address if its actual name, not truncated address.
+ Text(parsedTransaction.namedFromAddress)
+ .font(.footnote)
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ }
+ }
+ }
+ Spacer()
+ WalletIconButton(braveSystemName: "leo.copy") {
+ UIPasteboard.general.string = parsedTransaction.fromAddress
+ }
+ }
+
+ Divider()
+
+ HStack {
+ VStack(alignment: .leading) {
+ rowTitle(Strings.Wallet.swapCryptoToTitle)
+ AddressView(address: parsedTransaction.toAddress) {
+ Text(parsedTransaction.toAddress.zwspOutput) // zwspOutput to avoid hyphen when wrapped
+ .font(.callout)
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ if isLocalAccount(
+ address: parsedTransaction.toAddress,
+ namedAddress: parsedTransaction.namedToAddress
+ ) { // only show named address if its actual name, not truncated address.
+ Text(parsedTransaction.namedToAddress)
+ .font(.footnote)
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ }
+ }
+ }
+ Spacer()
+ WalletIconButton(braveSystemName: "leo.copy") {
+ UIPasteboard.general.string = parsedTransaction.toAddress
+ }
+ }
+
+ Divider()
+
+ if let gasFee = parsedTransaction.gasFee {
+ VStack(alignment: .leading) {
+ rowTitle(Strings.Wallet.transactionFee)
+ Text("\(gasFee.fee) \(parsedTransaction.network.nativeToken.symbol) ")
+ .foregroundColor(Color(braveSystemName: .textPrimary)) + Text("(\(gasFee.fiat))")
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ .font(.callout)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+ }
+ .padding(.vertical, 24)
+ .padding(.horizontal, 16)
+ }
+ .background(Color(braveSystemName: .containerBackground))
+ .onAppear {
+ transactionDetailsStore.update()
+ }
}
- private let dateFormatter = DateFormatter().then {
- $0.dateFormat = "h:mm a - MMM d, yyyy"
+ private func isLocalAccount(address: String, namedAddress: String) -> Bool {
+ if namedAddress.caseInsensitiveCompare(address) == .orderedSame
+ || namedAddress.caseInsensitiveCompare(address.truncatedAddress) == .orderedSame {
+ return false
+ }
+ return true
}
-
- private var header: some View {
- TransactionHeader(
- fromAccountAddress: transactionDetailsStore.parsedTransaction?.fromAddress ?? "",
- fromAccountName: transactionDetailsStore.parsedTransaction?.namedFromAddress ?? "",
- toAccountAddress: transactionDetailsStore.parsedTransaction?.toAddress ?? "",
- toAccountName: transactionDetailsStore.parsedTransaction?.namedToAddress ?? "",
- originInfo: transactionDetailsStore.parsedTransaction?.transaction.originInfo,
- transactionType: transactionDetailsStore.title ?? "",
- value: transactionDetailsStore.value ?? "",
- fiat: transactionDetailsStore.fiat
- )
- .frame(maxWidth: .infinity)
- .padding(.vertical, 30)
+
+ private func rowTitle(_ title: String) -> Text {
+ Text(title)
+ .font(.callout.weight(.semibold))
+ .foregroundColor(Color(braveSystemName: .textTertiary))
}
+}
+
+private struct DetailsView: View {
+
+ let transaction: BraveWallet.TransactionInfo
+ let parsedTransaction: ParsedTransaction?
var body: some View {
- NavigationView {
- List {
- Section(
- header: header
- .resetListHeaderStyle()
- ) {
- if let transactionFee = transactionDetailsStore.gasFee {
- detailRow(title: Strings.Wallet.transactionDetailsTxFeeTitle, value: transactionFee)
+ VStack(spacing: 16) {
+ if transaction.isSend {
+ TransactionDetailsSendContent(
+ transaction: transaction,
+ parsedTransaction: parsedTransaction
+ )
+ } else if transaction.isApprove {
+ TransactionDetailsApproveContent(
+ transaction: transaction,
+ parsedTransaction: parsedTransaction
+ )
+ } else if transaction.isSwap {
+ TransactionDetailsSwapContent(
+ transaction: transaction,
+ parsedTransaction: parsedTransaction
+ )
+ }
+
+ TransactionStatusBadgeView(status: parsedTransaction?.transaction.txStatus ?? transaction.txStatus)
+
+ VStack(spacing: 4) {
+ Text(transaction.createdTime, style: .date)
+ .font(.callout)
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ Text(parsedTransaction?.network.chainName ?? "")
+ .font(.footnote)
+ .foregroundColor(Color(braveSystemName: .textSecondary))
+ }
+ }
+ .padding(.horizontal)
+ .padding(.vertical, 24)
+ .frame(maxWidth: .infinity)
+ .background(background)
+ .cornerRadius(16)
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ }
+
+ private var background: some View {
+ Color(braveSystemName: .containerHighlight)
+ .overlay {
+ Color(braveSystemName: .containerBackground)
+ .mask {
+ Image("tx-details-lines", bundle: .module)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
}
- if let marketPrice = transactionDetailsStore.marketPrice {
- detailRow(title: Strings.Wallet.transactionDetailsMarketPriceTitle, value: marketPrice)
+ }
+ }
+}
+
+private struct TransactionDetailsSendContent: View {
+
+ var transaction: BraveWallet.TransactionInfo
+ var parsedTransaction: ParsedTransaction?
+
+ /// Send value including symbol when available, or name for NFTs
+ private var transactionValue: String {
+ guard let parsedTransaction else { return "" }
+ switch parsedTransaction.details {
+ case .ethSend(let details),
+ .erc20Transfer(let details),
+ .solSystemTransfer(let details),
+ .solSplTokenTransfer(let details):
+ if let fromToken = details.fromToken {
+ if fromToken.isNft || fromToken.isErc721 || fromToken.isErc1155 {
+ return fromToken.name
+ }
+ return String(format: "%@ %@", details.fromAmount, fromToken.symbol)
+ }
+ return details.fromAmount
+ case .filSend(let details):
+ if let sendToken = details.sendToken {
+ return String(format: "%@ %@", details.sendAmount, sendToken.symbol)
+ }
+ return details.sendAmount
+ case .solDappTransaction(let details):
+ if let symbol = details.symbol {
+ return String(format: "%@ %@", details.fromAmount, symbol)
+ }
+ return details.fromAmount
+ case .erc721Transfer(let details):
+ if let token = details.fromToken {
+ return token.name
+ }
+ return ""
+ case .ethSwap, .solSwapTransaction, .ethErc20Approve, .other:
+ // should not be used in this view...
+ return ""
+ }
+ }
+
+ /// Fiat value or symbol for NFTs
+ private var transactionFiatValue: String? {
+ guard let parsedTransaction else { return nil }
+ switch parsedTransaction.details {
+ case .ethSend(let details),
+ .erc20Transfer(let details),
+ .solSystemTransfer(let details),
+ .solSplTokenTransfer(let details):
+ if let fromToken = details.fromToken,
+ (fromToken.isNft || fromToken.isErc721 || fromToken.isErc1155) {
+ return fromToken.symbol
+ }
+ return details.fromFiat
+ case .filSend(let details):
+ return details.sendFiat
+ case .solDappTransaction, .erc721Transfer:
+ return nil // unknown fiat
+ default:
+ return nil
+ }
+ }
+
+ private var tokenSent: BraveWallet.BlockchainToken? {
+ switch parsedTransaction?.details {
+ case .ethSend(let details),
+ .erc20Transfer(let details),
+ .solSystemTransfer(let details),
+ .solSplTokenTransfer(let details):
+ return details.fromToken
+ case .filSend(let details):
+ return details.sendToken
+ case .erc721Transfer(let details):
+ return details.fromToken
+ case .solDappTransaction:
+ return nil // unknown token
+ default:
+ return nil
+ }
+ }
+
+ private var nftMetadataUrl: URL? {
+ switch parsedTransaction?.details {
+ case .solSplTokenTransfer(let details):
+ return details.fromTokenMetadata?.imageURL
+ case .erc721Transfer(let details):
+ return details.nftMetadata?.imageURL
+ default:
+ return nil
+ }
+ }
+
+ @ScaledMetric private var nftLength: CGFloat = 128
+ private let maxNFTLength: CGFloat = 160
+
+ var body: some View {
+ VStack(spacing: 4) {
+ if let token = tokenSent, let network = parsedTransaction?.network {
+ if token.isNft {
+ NFTIconView(
+ token: token,
+ network: network,
+ url: nftMetadataUrl,
+ shouldShowNetworkIcon: false,
+ length: nftLength,
+ maxLength: maxNFTLength
+ )
+ } else {
+ AssetIconView(
+ token: token,
+ network: network
+ )
+ }
+ } else {
+ GenericAssetIconView()
+ }
+ Text(Strings.Wallet.sent)
+ .font(.callout.weight(.semibold))
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ Text(transactionValue)
+ .font(.body.weight(.medium))
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ if let transactionFiatValue {
+ Text(transactionFiatValue)
+ .font(.footnote)
+ .foregroundColor(Color(braveSystemName: .textSecondary))
+ }
+ }
+ }
+}
+
+private struct TransactionDetailsApproveContent: View {
+
+ var transaction: BraveWallet.TransactionInfo
+ var parsedTransaction: ParsedTransaction?
+
+ private var approvalValue: String? {
+ if case .ethErc20Approve(let details) = parsedTransaction?.details {
+ if let token = details.token {
+ return "\(details.approvalAmount) \(token.symbol)"
+ }
+ return details.approvalAmount
+ }
+ return nil
+ }
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Circle()
+ .fill(Color(braveSystemName: .containerBackground))
+ .frame(width: 40, height: 40)
+ .overlay {
+ Image(braveSystemName: "leo.check.normal")
+ .imageScale(.large)
+ .padding()
+ }
+ .shadow(color: .black.opacity(0.07), radius: 2, x: 0, y: 1)
+ Text(Strings.Wallet.transactionTypeApprove)
+ .font(.callout.weight(.semibold))
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ if let approvalValue {
+ Text(approvalValue)
+ .font(.body.weight(.medium))
+ .foregroundColor(Color(braveSystemName: .textPrimary))
+ }
+ }
+ }
+}
+
+private struct TransactionDetailsSwapContent: View {
+
+ var transaction: BraveWallet.TransactionInfo
+ var parsedTransaction: ParsedTransaction?
+
+ @ScaledMetric(relativeTo: .body) private var assetIconLength: CGFloat = 32
+ private let maxAssetIconLength: CGFloat = 64
+
+ private var fromToken: BraveWallet.BlockchainToken? {
+ if case .ethSwap(let details) = parsedTransaction?.details {
+ return details.fromToken
+ }
+ return nil
+ }
+
+ private var fromAmount: String? {
+ if case .ethSwap(let details) = parsedTransaction?.details {
+ return details.fromAmount
+ }
+ return nil
+ }
+
+ private var toToken: BraveWallet.BlockchainToken? {
+ if case .ethSwap(let details) = parsedTransaction?.details {
+ return details.toToken
+ }
+ return nil
+ }
+
+ private var minBuyAmount: String? {
+ if case .ethSwap(let details) = parsedTransaction?.details {
+ return details.minBuyAmount
+ }
+ return nil
+ }
+
+ private var transactionFiatValue: String? {
+ if case .ethSwap(let details) = parsedTransaction?.details {
+ return details.fromFiat
+ }
+ return nil
+ }
+
+ private var isSolanaSwap: Bool {
+ transaction.txType == .solanaSwap
+ }
+
+ var body: some View {
+ VStack(spacing: 4) {
+ Text(isSolanaSwap ? Strings.Wallet.transactionSummarySolanaSwap : Strings.Wallet.swap)
+ .font(.callout.weight(.semibold))
+ .foregroundColor(Color(braveSystemName: .textTertiary))
+ if let parsedTransaction {
+ if isSolanaSwap {
+ HStack {
+ GenericAssetIconView(
+ backgroundColor: Color(braveSystemName: .gray40),
+ iconColor: Color.white,
+ length: assetIconLength,
+ maxLength: maxAssetIconLength
+ )
+ Image(braveSystemName: "leo.arrow.right")
+ GenericAssetIconView(
+ backgroundColor: Color(braveSystemName: .gray20),
+ iconColor: Color.black,
+ length: assetIconLength,
+ maxLength: maxAssetIconLength
+ )
}
- detailRow(title: Strings.Wallet.transactionDetailsDateTitle, value: dateFormatter.string(from: transactionDetailsStore.transaction.createdTime))
- if !transactionDetailsStore.transaction.txHash.isEmpty {
- Button(action: {
- if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == transactionDetailsStore.transaction.chainId }),
- let url = txNetwork.txBlockExplorerLink(txHash: transactionDetailsStore.transaction.txHash, for: txNetwork.coin) {
- openWalletURL(url)
+ } else {
+ VStack {
+ HStack {
+ if let fromToken {
+ AssetIconView(
+ token: fromToken,
+ network: parsedTransaction.network,
+ length: assetIconLength,
+ maxLength: maxAssetIconLength
+ )
+ } else {
+ GenericAssetIconView(length: assetIconLength, maxLength: maxAssetIconLength)
}
- }) {
- detailRow(title: Strings.Wallet.transactionDetailsTxHashTitle) {
- HStack {
- Text(transactionDetailsStore.transaction.txHash.truncatedHash)
- Image(systemName: "arrow.up.forward.square")
+ if let fromAmount {
+ if let fromToken {
+ Text(verbatim: "\(fromAmount) \(fromToken.symbol)")
+ } else {
+ Text(verbatim: fromAmount)
}
- .foregroundColor(Color(.braveBlurpleTint))
}
}
- .listRowBackground(Color(.secondaryBraveGroupedBackground))
- }
- if let network = transactionDetailsStore.network {
- detailRow(title: Strings.Wallet.transactionDetailsNetworkTitle, value: network.chainName)
- }
- detailRow(title: Strings.Wallet.transactionDetailsStatusTitle) {
- HStack(spacing: 4) {
- Image(systemName: "circle.fill")
- .foregroundColor(transactionDetailsStore.transaction.txStatus.color)
- .imageScale(.small)
- .accessibilityHidden(true)
- Text(transactionDetailsStore.transaction.txStatus.localizedDescription)
- .foregroundColor(Color(.braveLabel))
- .multilineTextAlignment(.trailing)
+ Image(braveSystemName: "leo.arrow.right")
+ HStack {
+ if let toToken {
+ AssetIconView(
+ token: toToken,
+ network: parsedTransaction.network,
+ length: assetIconLength,
+ maxLength: maxAssetIconLength
+ )
+ } else {
+ GenericAssetIconView(length: assetIconLength, maxLength: maxAssetIconLength)
+ }
+ if let minBuyAmount {
+ if let toToken {
+ Text(verbatim: "\(minBuyAmount) \(toToken.symbol)")
+ } else {
+ Text(verbatim: minBuyAmount)
+ }
+ }
}
- .accessibilityElement(children: .combine)
- .font(.caption.weight(.semibold))
- }
- }
- .listRowInsets(.zero)
- }
- .listStyle(.insetGrouped)
- .listBackgroundColor(Color(UIColor.braveGroupedBackground))
- .background(Color(.braveGroupedBackground).edgesIgnoringSafeArea(.all))
- .navigationTitle(Strings.Wallet.transactionDetailsTitle)
- .navigationBarTitleDisplayMode(.inline)
- .navigationViewStyle(.stack)
- .toolbar {
- ToolbarItemGroup(placement: .confirmationAction) {
- Button(action: { presentationMode.dismiss() }) {
- Text(Strings.done)
- .foregroundColor(Color(.braveBlurpleTint))
}
+ .font(.body.weight(.medium))
}
}
- .onAppear(perform: transactionDetailsStore.update)
+ if let transactionFiatValue {
+ Text(transactionFiatValue)
+ .font(.footnote)
+ .foregroundColor(Color(braveSystemName: .textSecondary))
+ }
}
}
+}
+
+private struct TransactionStatusBadgeView: View {
- private func detailRow(title: String, value: String) -> some View {
- detailRow(title: title) {
- Text(value)
- .multilineTextAlignment(.trailing)
- }
- }
+ let status: BraveWallet.TransactionStatus
- private func detailRow(title: String, @ViewBuilder valueView: () -> ValueView) -> some View {
+ var body: some View {
HStack {
- Text(title)
- Spacer()
- valueView()
+ if status.shouldShowLoadingStatus {
+ ProgressView()
+ .progressViewStyle(.braveCircular(size: .mini))
+ } else if status.shouldShowSuccessStatus {
+ Image(braveSystemName: "leo.check.circle-outline")
+ .foregroundColor(Color(braveSystemName: .systemfeedbackSuccessIcon))
+ } else if status.shouldShowErrorStatus {
+ Image(braveSystemName: "leo.warning.circle-outline")
+ .foregroundColor(Color(braveSystemName: .systemfeedbackErrorIcon))
+ }
+ Text(status.localizedDescription)
+ .foregroundColor(status.badgeTextColor)
}
.font(.caption)
- .foregroundColor(Color(.braveLabel))
- .padding(.horizontal)
- .padding(.vertical, 12)
- .listRowBackground(Color(.secondaryBraveGroupedBackground))
+ .padding(.horizontal, 6)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 4)
+ .fill(status.badgeBackgroundColor)
+ )
+ }
+}
+
+private extension BraveWallet.TransactionStatus {
+ /// If we should show transaction status as loading
+ var shouldShowLoadingStatus: Bool {
+ switch self {
+ case .unapproved, .submitted:
+ return true
+ default:
+ return false
+ }
+ }
+
+ /// If we should show transaction status successful icon
+ var shouldShowSuccessStatus: Bool {
+ switch self {
+ case .approved, .confirmed, .signed:
+ return true
+ default:
+ return false
+ }
+ }
+
+ /// If we should show transaction status failure icon
+ var shouldShowErrorStatus: Bool {
+ switch self {
+ case .error, .dropped, .rejected:
+ return true
+ default:
+ return false
+ }
+ }
+
+ /// Color of status text on status badge
+ var badgeTextColor: Color {
+ switch self {
+ case .confirmed, .approved:
+ return Color(braveSystemName: .systemfeedbackSuccessText)
+ case .rejected, .error, .dropped:
+ return Color(braveSystemName: .systemfeedbackErrorText)
+ case .unapproved:
+ return Color(braveSystemName: .textSecondary)
+ case .submitted, .signed:
+ return Color(braveSystemName: .systemfeedbackInfoText)
+ @unknown default:
+ return Color.clear
+ }
+ }
+
+ /// Color of status badge
+ var badgeBackgroundColor: Color {
+ switch self {
+ case .confirmed, .approved:
+ return Color(braveSystemName: .systemfeedbackSuccessBackground)
+ case .rejected, .error, .dropped:
+ return Color(braveSystemName: .systemfeedbackErrorBackground)
+ case .unapproved:
+ return Color(braveSystemName: .dividerStrong)
+ case .submitted, .signed:
+ return Color(braveSystemName: .systemfeedbackInfoBackground)
+ @unknown default:
+ return Color.clear
+ }
}
}
diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift
index c4a27c771d7..daaac3af6db 100644
--- a/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift
+++ b/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift
@@ -87,7 +87,7 @@ struct TransactionsListView: View {
VStack(spacing: 0) {
HStack(spacing: 10) {
SearchBar(text: $query, placeholder: Strings.Wallet.search)
- AssetButton(braveSystemName: "leo.filter.settings", action: filtersButtonTapped)
+ WalletIconButton(braveSystemName: "leo.filter.settings", action: filtersButtonTapped)
}
.padding(.vertical, 8)
Divider()
diff --git a/Sources/BraveWallet/Crypto/TransactionsActivityView.swift b/Sources/BraveWallet/Crypto/TransactionsActivityView.swift
index 33559b94804..dcf746449e9 100644
--- a/Sources/BraveWallet/Crypto/TransactionsActivityView.swift
+++ b/Sources/BraveWallet/Crypto/TransactionsActivityView.swift
@@ -46,7 +46,12 @@ struct TransactionsActivityView: View {
.sheet(
isPresented: Binding(
get: { self.transactionDetails != nil },
- set: { if !$0 { self.transactionDetails = nil } }
+ set: {
+ if !$0 {
+ self.transactionDetails = nil
+ self.store.closeTransactionDetailsStore()
+ }
+ }
)
) {
if let transactionDetailsStore = self.transactionDetails {
diff --git a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift
index f3248963022..e061bc06ddf 100644
--- a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift
+++ b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift
@@ -16,6 +16,37 @@ extension BraveWallet.TransactionInfo {
return false
}
}
+
+ var isApprove: Bool {
+ txType == .erc20Approve
+ }
+
+ var isSend: Bool {
+ switch txType {
+ case .ethSend,
+ .erc20Transfer,
+ .erc721TransferFrom,
+ .erc721SafeTransferFrom,
+ .erc1155SafeTransferFrom,
+ .solanaSystemTransfer,
+ .solanaSplTokenTransfer,
+ .solanaSplTokenTransferWithAssociatedTokenAccountCreation,
+ .solanaDappSignAndSendTransaction,
+ .solanaDappSignTransaction,
+ .ethFilForwarderTransfer:
+ return true
+ case .other:
+ // Filecoin send
+ return txDataUnion.filTxData != nil
+ case .erc20Approve,
+ .ethSwap,
+ .solanaSwap:
+ return false
+ @unknown default:
+ return false
+ }
+ }
+
var isEIP1559Transaction: Bool {
if coin == .eth {
guard let ethTxData1559 = txDataUnion.ethTxData1559 else { return false }
diff --git a/Sources/BraveWallet/Preview Content/MockStores.swift b/Sources/BraveWallet/Preview Content/MockStores.swift
index 25dbce3b95c..94738798802 100644
--- a/Sources/BraveWallet/Preview Content/MockStores.swift
+++ b/Sources/BraveWallet/Preview Content/MockStores.swift
@@ -144,6 +144,7 @@ extension AssetDetailStore {
txService: MockTxService(),
blockchainRegistry: MockBlockchainRegistry(),
solTxManagerProxy: BraveWallet.TestSolanaTxManagerProxy.previewProxy,
+ ipfsApi: TestIpfsAPI(),
swapService: MockSwapService(),
userAssetManager: TestableWalletUserAssetManager(),
assetDetailType: .blockchainToken(.previewToken)
@@ -215,6 +216,7 @@ extension TransactionConfirmationStore {
return service
}(),
solTxManagerProxy: BraveWallet.TestSolanaTxManagerProxy.previewProxy,
+ ipfsApi: TestIpfsAPI(),
userAssetManager: TestableWalletUserAssetManager()
)
}
diff --git a/Sources/BraveWallet/WalletHostingViewController.swift b/Sources/BraveWallet/WalletHostingViewController.swift
index 03ca32bc04c..14e202d37e5 100644
--- a/Sources/BraveWallet/WalletHostingViewController.swift
+++ b/Sources/BraveWallet/WalletHostingViewController.swift
@@ -94,7 +94,7 @@ public class WalletHostingViewController: UIHostingController {
// As a workaround to this issue, we can just watch keyring's `isLocked` value from here
// and dismiss the first sheet ourselves to ensure we dont get stuck with a child view visible
// while the wallet is locked.
- if #unavailable(iOS 16.4),
+ if /*#unavailable(iOS 16.4),*/
let self = self,
isLocked,
let presentedViewController = self.presentedViewController,
diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift
index 6e326deadad..0443f161c28 100644
--- a/Sources/BraveWallet/WalletStrings.swift
+++ b/Sources/BraveWallet/WalletStrings.swift
@@ -2400,7 +2400,7 @@ extension Strings {
"wallet.transactionDetailsTransactionHashTitle",
tableName: "BraveWallet",
bundle: .module,
- value: "Transaction hash",
+ value: "Transaction Hash",
comment: "The label for the transaction hash (the identifier) of a cryptocurrency transaction. Appears next to a button that opens a URL for the transaction."
)
public static let transactionDetailsStatusTitle = NSLocalizedString(
diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.arrow.right.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.arrow.right.symbolset/Contents.json
new file mode 100644
index 00000000000..2f415ce6e4c
--- /dev/null
+++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.arrow.right.symbolset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.copy.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.copy.symbolset/Contents.json
new file mode 100644
index 00000000000..2f415ce6e4c
--- /dev/null
+++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.copy.symbolset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.warning.circle-outline.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.warning.circle-outline.symbolset/Contents.json
new file mode 100644
index 00000000000..2f415ce6e4c
--- /dev/null
+++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.warning.circle-outline.symbolset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Tests/BraveWalletTests/AssetDetailStoreTests.swift b/Tests/BraveWalletTests/AssetDetailStoreTests.swift
index 65531e42117..4c11fb63b6f 100644
--- a/Tests/BraveWalletTests/AssetDetailStoreTests.swift
+++ b/Tests/BraveWalletTests/AssetDetailStoreTests.swift
@@ -91,6 +91,7 @@ class AssetDetailStoreTests: XCTestCase {
txService: txService,
blockchainRegistry: blockchainRegistry,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: TestIpfsAPI(),
swapService: swapService,
userAssetManager: mockAssetManager,
assetDetailType: .blockchainToken(.previewToken)
@@ -284,6 +285,7 @@ class AssetDetailStoreTests: XCTestCase {
txService: txService,
blockchainRegistry: blockchainRegistry,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: TestIpfsAPI(),
swapService: swapService,
userAssetManager: mockAssetManager,
assetDetailType: .coinMarket(.mockCoinMarketBitcoin)
@@ -401,6 +403,7 @@ class AssetDetailStoreTests: XCTestCase {
txService: txService,
blockchainRegistry: blockchainRegistry,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: TestIpfsAPI(),
swapService: swapService,
userAssetManager: mockAssetManager,
assetDetailType: .coinMarket(.mockCoinMarketEth)
diff --git a/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift b/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift
index 442f6526a20..c75e81f93ff 100644
--- a/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift
+++ b/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift
@@ -130,6 +130,7 @@ import Preferences
ethTxManagerProxy: ethTxManagerProxy,
keyringService: keyringService,
solTxManagerProxy: solTxManagerProxy,
+ ipfsApi: TestIpfsAPI(),
userAssetManager: mockAssetManager
)
}