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 ) }