diff --git a/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift b/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift index 1469b036bd6..7a577ee2395 100644 --- a/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift +++ b/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift @@ -51,6 +51,26 @@ struct NFTDetailView: View { .frame(maxWidth: .infinity, minHeight: 300) } else { nftImage + .overlay(alignment: .topLeading) { + if nftDetailStore.nft.isSpam { + HStack(spacing: 4) { + Text(Strings.Wallet.nftSpam) + .padding(.vertical, 4) + .padding(.leading, 6) + .foregroundColor(Color(.braveErrorLabel)) + Image(braveSystemName: "leo.warning.triangle-outline") + .padding(.vertical, 4) + .padding(.trailing, 6) + .foregroundColor(Color(.braveErrorBorder)) + } + .font(.system(size: 13).weight(.semibold)) + .background( + Color(uiColor: WalletV2Design.spamNFTLabelBackground) + .cornerRadius(4) + ) + .padding(12) + } + } } VStack(alignment: .leading, spacing: 8) { Text(nftDetailStore.nft.nftTokenTitle) diff --git a/Sources/BraveWallet/Crypto/NFT/NFTView.swift b/Sources/BraveWallet/Crypto/NFT/NFTView.swift index 671881adefe..474809c1ff1 100644 --- a/Sources/BraveWallet/Crypto/NFT/NFTView.swift +++ b/Sources/BraveWallet/Crypto/NFT/NFTView.swift @@ -20,6 +20,7 @@ struct NFTView: View { @State private var isShowingNFTDiscoveryAlert: Bool = false @State private var isShowingAddCustomNFT: Bool = false @State private var isNFTDiscoveryEnabled: Bool = false + @State private var nftToBeRemoved: NFTAssetViewModel? @Environment(\.buySendSwapDestination) private var buySendSwapDestination: Binding @@ -243,7 +244,7 @@ struct NFTView: View { } } Button(action: { - nftStore.updateNFTStatus(nft.token, visible: false, isSpam: nft.token.isSpam, isDeletedByUser: true) + nftToBeRemoved = nft }) { Label(Strings.Wallet.nftRemoveFromWallet, braveSystemImage: "leo.trash") } @@ -331,6 +332,38 @@ struct NFTView: View { } ) ) + .background( + WalletPromptView( + isPresented: Binding( + get: { nftToBeRemoved != nil }, + set: { if !$0 { nftToBeRemoved = nil } } + ), + primaryButton: .init( + title: Strings.Wallet.manageSiteConnectionsConfirmAlertRemove, + action: { _ in + guard let nft = nftToBeRemoved else { return } + nftStore.updateNFTStatus(nft.token, visible: false, isSpam: nft.token.isSpam, isDeletedByUser: true) + nftToBeRemoved = nil + } + ), + secondaryButton: .init( + title: Strings.CancelString, + action: { _ in + nftToBeRemoved = nil + } + ), + showCloseButton: false, + content: { + VStack(spacing: 16) { + Text(Strings.Wallet.nftRemoveFromWalletAlertTitle) + .font(.headline) + .foregroundColor(Color(.bravePrimary)) + Text(Strings.Wallet.nftRemoveFromWalletAlertDescription) + .font(.footnote) + .foregroundStyle(Color(.secondaryBraveLabel)) + } + }) + ) .sheet(isPresented: $isShowingAddCustomNFT) { AddCustomAssetView( networkStore: networkStore, diff --git a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift index e0c1f045b9e..357e403dc48 100644 --- a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift @@ -141,7 +141,7 @@ class AccountActivityStore: ObservableObject, WalletObserverStore { let tokens: [BraveWallet.BlockchainToken] let sortOrder: Int } - let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networksForAccount, includingSpam: true) + let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networksForAccount, includingUserDeleted: true) let allTokens = await blockchainRegistry.allTokens(in: networksForAccountCoin).flatMap(\.tokens) var updatedUserAssets: [AssetViewModel] = [] var updatedUserNFTs: [NFTAssetViewModel] = [] diff --git a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift index 1dcd31e58d4..b043d3de902 100644 --- a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift @@ -317,7 +317,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { ) async -> [TransactionSummary] { guard case let .blockchainToken(token) = assetDetailType else { return [] } - let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens } + let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens } let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) let allTransactions = await withTaskGroup(of: [BraveWallet.TransactionInfo].self) { @MainActor group -> [BraveWallet.TransactionInfo] in for account in keyring.accountInfos { diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift index af314b6a65b..00035fff68c 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift @@ -277,7 +277,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore let transactionNetworks: [BraveWallet.NetworkInfo] = Set(allTxs.map(\.chainId)) .compactMap { chainId in allNetworks.first(where: { $0.chainId == chainId }) } for network in transactionNetworks { - let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens } + let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens } await fetchAssetRatios(for: userAssets) } await fetchUnknownTokens(for: unapprovedTxs) @@ -307,7 +307,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore return } let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) + tokenInfoCache.map(\.value) - let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens } + let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens } let solEstimatedTxFee: UInt64? = solEstimatedTxFeeCache[transaction.id] if transaction.isEIP1559Transaction { @@ -431,7 +431,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore guard let network = allNetworks.first(where: { $0.chainId == BraveWallet.MainnetChainId }) else { return } - let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens } + let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens } let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) let unknownTokenContractAddresses = mainnetTransactions.flatMap(\.tokenContractAddresses) .filter { contractAddress in diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift index 08ff67f8eb0..35836ecd6a9 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift @@ -80,7 +80,7 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore { let keringId = BraveWallet.KeyringId.keyringId(for: coin, on: transaction.chainId) let keyring = await keyringService.keyringInfo(keringId) var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) + tokenInfoCache.map(\.value) - let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens } + let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens } let unknownTokenContractAddresses = transaction.tokenContractAddresses .filter { contractAddress in !userAssets.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame }) diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift index 62b4a7097ed..2230020bfe0 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift @@ -138,7 +138,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore { let allTransactions = await txService.allTransactions( networksForCoin: networksForCoin, for: allKeyrings ).filter { $0.txStatus != .rejected } - let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworksAllCoins, includingSpam: true).flatMap(\.tokens) + let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworksAllCoins, includingUserDeleted: true).flatMap(\.tokens) let allTokens = await blockchainRegistry.allTokens( in: allNetworksAllCoins ).flatMap(\.tokens) diff --git a/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift b/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift index ed871d47243..b0345dc4769 100644 --- a/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift @@ -137,7 +137,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore { } } let networks: [BraveWallet.NetworkInfo] = self.networkFilters.filter(\.isSelected).map(\.model) - let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networks, includingSpam: true) + let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networks, includingUserDeleted: false) var allTokens = await self.blockchainRegistry.allTokens(in: networks) // Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token let allUserTokens = allUserAssets.flatMap(\.tokens) @@ -233,7 +233,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore { @MainActor func allAssets() async -> [AssetViewModel] { let allNetworks = await rpcService.allNetworksForSupportedCoins() - let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingSpam: true) + let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingUserDeleted: false) // Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token let allUserTokens = allUserAssets.flatMap(\.tokens) let allBlockchainTokens = await blockchainRegistry.allTokens(in: allNetworks) @@ -262,7 +262,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore { @MainActor func allNFTMetadata() async -> [String: NFTMetadata] { let allNetworks = await rpcService.allNetworksForSupportedCoins() - let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingSpam: true) + let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingUserDeleted: true) // Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token let allUserTokens = allUserAssets.flatMap(\.tokens) diff --git a/Sources/BraveWallet/Extensions/WalletColors.swift b/Sources/BraveWallet/Extensions/WalletColors.swift index 8e47d5e8409..b7c57b5d001 100644 --- a/Sources/BraveWallet/Extensions/WalletColors.swift +++ b/Sources/BraveWallet/Extensions/WalletColors.swift @@ -252,7 +252,7 @@ enum WalletV2Design { static let passwordStrongGreen = UIColor(rgb: 0x31803e) static let spamNFTLabelBackground = UIColor( - red: 255 / 255, + red: 1, green: 209 / 255, blue: 207 / 255, alpha: 1 diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index 783119ec86a..81eaf7b2cb2 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -4360,6 +4360,20 @@ extension Strings { value: "Don't show in wallet", comment: "The title of context button for user to do not show a NFT in wallet at all." ) + public static let nftRemoveFromWalletAlertTitle = NSLocalizedString( + "wallet.nftRemoveFromWalletAlertTitle", + tableName: "BraveWallet", + bundle: .module, + value: "Remove from Brave Wallet?", + comment: "The title of the alert when user attempts to remove an NFT from wallet." + ) + public static let nftRemoveFromWalletAlertDescription = NSLocalizedString( + "wallet.nftRemoveFromWalletAlertDescription", + tableName: "BraveWallet", + bundle: .module, + value: "NFT will be removed from Brave Wallet but will remain on the blockchain. If you remove it, then change your mind, you'll need to import it again manually.", + comment: "The description of the alert when user attempts to remove an NFT from wallet." + ) public static let selectTokenToSendTitle = NSLocalizedString( "wallet.selectTokenToSendTitle", tableName: "BraveWallet", diff --git a/Sources/BraveWallet/WalletUserAssetManager.swift b/Sources/BraveWallet/WalletUserAssetManager.swift index bdc1c7c6121..be0ec12a1ed 100644 --- a/Sources/BraveWallet/WalletUserAssetManager.swift +++ b/Sources/BraveWallet/WalletUserAssetManager.swift @@ -13,7 +13,7 @@ public protocol WalletUserAssetManagerType: AnyObject { /// Return all visible or all invisible user assets in form of `NetworkAssets` func getAllUserAssetsInNetworkAssetsByVisibility(networks: [BraveWallet.NetworkInfo], visible: Bool) -> [NetworkAssets] /// Return all user assets in form of `NetworkAssets` - func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingSpam: Bool) -> [NetworkAssets] + func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingUserDeleted: Bool) -> [NetworkAssets] /// Return all spam or non-spam user assets in form of `NetworkAssets` func getAllUserNFTs(networks: [BraveWallet.NetworkInfo], isSpam: Bool) -> [NetworkAssets] /// Return all user marked deleted user assets @@ -46,13 +46,13 @@ public class WalletUserAssetManager: WalletUserAssetManagerType { } /// Return all user's assets stored in CoreData - public func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingSpam: Bool) -> [NetworkAssets] { + public func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingUserDeleted: Bool) -> [NetworkAssets] { var allUserAssets: [NetworkAssets] = [] for (index, network) in networks.enumerated() { let groupId = network.walletUserAssetGroupId if let walletUserAssets = WalletUserAssetGroup.getGroup(groupId: groupId)?.walletUserAssets?.filter({ - if !includingSpam { - return $0.isSpam == false + if !includingUserDeleted { + return $0.isDeletedByUser == false } return true }) { @@ -114,7 +114,7 @@ public class WalletUserAssetManager: WalletUserAssetManagerType { if let existedAsset = WalletUserAsset.getUserAsset(asset: asset) { if existedAsset.isDeletedByUser { // this asset was added before but user marked as deleted after WalletUserAsset.updateUserAsset(for: asset, visible: true, isSpam: false, isDeletedByUser: false, completion: completion) - } else { // this asset exists, either in `Collected` or `Hidden` based on its `visible` value + } else { // this asset already exists completion?() return } @@ -195,19 +195,19 @@ public class WalletUserAssetManager: WalletUserAssetManagerType { #if DEBUG public class TestableWalletUserAssetManager: WalletUserAssetManagerType { public var _getAllUserAssetsInNetworkAssetsByVisibility: ((_ networks: [BraveWallet.NetworkInfo], _ visible: Bool) -> [NetworkAssets])? - public var _getAllUserAssetsInNetworkAssets: ((_ networks: [BraveWallet.NetworkInfo], _ includingSpam: Bool) -> [NetworkAssets])? + public var _getAllUserAssetsInNetworkAssets: ((_ networks: [BraveWallet.NetworkInfo], _ includingUserDeleted: Bool) -> [NetworkAssets])? public var _getAllUserNFTs: ((_ networks: [BraveWallet.NetworkInfo], _ spamStatus: Bool) -> [NetworkAssets])? public var _getAllUserDeletedNFTs: (() -> [WalletUserAsset])? public init() {} - public func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingSpam: Bool) -> [NetworkAssets] { + public func getAllUserAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo], includingUserDeleted: Bool) -> [NetworkAssets] { let defaultAssets: [NetworkAssets] = [ NetworkAssets(network: .mockMainnet, tokens: [.previewToken], sortOrder: 0), NetworkAssets(network: .mockGoerli, tokens: [.previewToken], sortOrder: 1) ] let chainIds = networks.map { $0.chainId } - return _getAllUserAssetsInNetworkAssets?(networks, includingSpam) ?? defaultAssets.filter({ + return _getAllUserAssetsInNetworkAssets?(networks, includingUserDeleted) ?? defaultAssets.filter({ chainIds.contains($0.network.chainId) }) }