diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/Contents.json b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/Contents.json new file mode 100644 index 00000000000..9fb58dd5aab --- /dev/null +++ b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "account-empty-light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "account-empty-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "account-empty-light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "account-empty-dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "account-empty-light@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "account-empty-dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark.png new file mode 100644 index 00000000000..50a523c343f Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@2x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@2x.png new file mode 100644 index 00000000000..4248793d5d2 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@2x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@3x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@3x.png new file mode 100644 index 00000000000..87278aedb76 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-dark@3x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light.png new file mode 100644 index 00000000000..f7bbd5d23b5 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@2x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@2x.png new file mode 100644 index 00000000000..35271ae57d2 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@2x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@3x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@3x.png new file mode 100644 index 00000000000..154eba4c73a Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/account-empty.imageset/account-empty-light@3x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/Contents.json b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/Contents.json new file mode 100644 index 00000000000..ebf8bab7025 --- /dev/null +++ b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "transaction-empty-light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "transaction-empty-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "transaction-empty-light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "transaction-empty-dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "transaction-empty-light@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "transaction-empty-dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark.png new file mode 100644 index 00000000000..fcdddc96498 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@2x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@2x.png new file mode 100644 index 00000000000..3f8552f8fa5 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@2x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@3x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@3x.png new file mode 100644 index 00000000000..5d83f416d1a Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-dark@3x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light.png new file mode 100644 index 00000000000..bc8dea24a80 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@2x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@2x.png new file mode 100644 index 00000000000..e9da96da67b Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@2x.png differ diff --git a/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@3x.png b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@3x.png new file mode 100644 index 00000000000..c1d42eb3609 Binary files /dev/null and b/Sources/BraveWallet/Assets.xcassets/Empty States/transaction-empty.imageset/transaction-empty-light@3x.png differ diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift index db6fe9613d9..c6807b4709f 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift @@ -23,9 +23,10 @@ struct AccountView: View { } VStack(alignment: .leading, spacing: 2) { Text(name) - .fontWeight(.semibold) + .font(.subheadline.weight(.semibold)) .foregroundColor(Color(.bravePrimary)) Text(address.truncatedAddress) + .font(.footnote) .foregroundColor(Color(.braveLabel)) } .font(.caption) diff --git a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailHeaderView.swift b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailHeaderView.swift index 0e386bcfece..e3b18604b8a 100644 --- a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailHeaderView.swift +++ b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailHeaderView.swift @@ -20,9 +20,6 @@ struct AssetDetailHeaderView: View { @ObservedObject var assetDetailStore: AssetDetailStore @ObservedObject var keyringStore: KeyringStore @ObservedObject var networkStore: NetworkStore - @Binding var buySendSwapDestination: BuySendSwapDestination? - @Binding var isShowingBridgeAlert: Bool - var onAccountCreationNeeded: (_ savedDestination: BuySendSwapDestination) -> Void @Environment(\.sizeCategory) private var sizeCategory @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -35,15 +32,10 @@ struct AssetDetailHeaderView: View { Image(systemName: assetDetailStore.priceIsDown ? "arrow.down" : "arrow.up") Text(assetDetailStore.priceDelta) } - .foregroundColor(.white) - .font(.caption2) - .padding(5) - .background( - Color( - assetDetailStore.priceIsDown ? .walletRed : .walletGreen - ) - .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) - ) + .foregroundColor(Color( + assetDetailStore.priceIsDown ? .walletRed : .walletGreen + )) + .font(.footnote) } private var emptyData: [BraveWallet.AssetTimePrice] { @@ -51,228 +43,117 @@ struct AssetDetailHeaderView: View { (0..<300).map { _ in .init(date: Date(), price: "0.0") } } - @ViewBuilder private var actionButtonsContainer: some View { - if assetDetailStore.isBuySupported && assetDetailStore.isSwapSupported { - VStack { - actionButtons - } - } else { - HStack { - actionButtons - } - } - } - - @ViewBuilder private var actionButtons: some View { - buySendSwapButtonsContainer - if case let .blockchainToken(token) = assetDetailStore.assetDetailType, token.isAuroraSupportedToken { - auroraBridgeButton - } - } - - @ViewBuilder var buySendSwapButtonsContainer: some View { + @ViewBuilder private var tokenInfoView: some View { HStack { - if assetDetailStore.isBuySupported { - Button( - action: { - let destination = BuySendSwapDestination( - kind: .buy, - initialToken: assetDetailStore.assetDetailToken - ) - if assetDetailStore.accounts.isEmpty { - onAccountCreationNeeded(destination) - } else { - buySendSwapDestination = destination + AssetIconView(token: assetDetailStore.assetDetailToken, network: assetDetailStore.network ?? networkStore.defaultSelectedChain) + if sizeCategory.isAccessibilityCategory { + VStack(alignment: .leading, spacing: 8) { + Group { + Text(assetDetailStore.assetDetailToken.name) + .fixedSize(horizontal: false, vertical: true) + .font(.body.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) + if let chainName = assetDetailStore.network?.chainName { + Text("\(assetDetailStore.assetDetailToken.symbol) on \(chainName)") + .fixedSize(horizontal: false, vertical: true) + .font(.footnote) + .foregroundColor(Color(.braveLabel)) + } + Group { + if let selectedCandle = selectedCandle, + let formattedString = assetDetailStore.currencyFormatter.string(from: NSNumber(value: selectedCandle.value)) { + Text(formattedString) + } else { + Text(assetDetailStore.price) + } } + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) } - ) { - Text(Strings.Wallet.buy) + .transaction { transaction in + transaction.animation = nil + transaction.disablesAnimations = true + } + deltaText + .redacted(reason: assetDetailStore.isInitialState ? .placeholder : []) + .shimmer(assetDetailStore.isLoadingPrice) } - } - if assetDetailStore.isSendSupported { - Button( - action: { - let destination = BuySendSwapDestination( - kind: .send, - initialToken: assetDetailStore.assetDetailToken - ) - if assetDetailStore.accounts.isEmpty { - onAccountCreationNeeded(destination) - } else { - buySendSwapDestination = destination + } else { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text(assetDetailStore.assetDetailToken.name) + .fixedSize(horizontal: false, vertical: true) + .font(.body.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) + if let chainName = assetDetailStore.network?.chainName { + Text("\(assetDetailStore.assetDetailToken.symbol) on \(chainName)") + .fixedSize(horizontal: false, vertical: true) + .font(.footnote) + .foregroundColor(Color(.braveLabel)) } } - ) { - Text(Strings.Wallet.send) } - } - if assetDetailStore.isSwapSupported && assetDetailStore.assetDetailToken.isFungibleToken { - Button( - action: { - let destination = BuySendSwapDestination( - kind: .swap, - initialToken: assetDetailStore.assetDetailToken - ) - if assetDetailStore.accounts.isEmpty { - onAccountCreationNeeded(destination) + Spacer() + VStack(alignment: .trailing, spacing: 8) { + Group { + if let selectedCandle = selectedCandle, + let formattedString = assetDetailStore.currencyFormatter.string(from: NSNumber(value: selectedCandle.value)) { + Text(formattedString) } else { - buySendSwapDestination = destination + Text(assetDetailStore.price) } } - ) { - Text(Strings.Wallet.swap) - } - } - } - .buttonStyle(BraveFilledButtonStyle(size: .normal)) - } - - @ViewBuilder var auroraBridgeButton: some View { - Button( - action: { - if Preferences.Wallet.showAuroraPopup.value { - isShowingBridgeAlert = true - } else { - if let link = WalletConstants.auroraBridgeLink { - openWalletURL(link) + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) + .transaction { transaction in + transaction.animation = nil + transaction.disablesAnimations = true } + deltaText + .redacted(reason: assetDetailStore.isInitialState ? .placeholder : []) + .shimmer(assetDetailStore.isLoadingPrice) } } - ) { - Text(Strings.Wallet.auroraBridgeButtonTitle) } - .buttonStyle(BraveFilledButtonStyle(size: .normal)) } - @ViewBuilder private var tokenImageNameAndNetwork: some View { - AssetIconView(token: assetDetailStore.assetDetailToken, network: assetDetailStore.network ?? networkStore.defaultSelectedChain) - VStack(alignment: .leading) { - Text(assetDetailStore.assetDetailToken.name) - .fixedSize(horizontal: false, vertical: true) - .font(.title3.weight(.semibold)) - if let chainName = assetDetailStore.network?.chainName { - Text(chainName) - .fixedSize(horizontal: false, vertical: true) - .font(.caption) + @ViewBuilder private var lineChart: some View { + VStack(spacing: 0) { + TimeframeSelector(selectedDateRange: $assetDetailStore.timeframe) + .padding(.top, 24) + let data = assetDetailStore.priceHistory.isEmpty ? emptyData : assetDetailStore.priceHistory + LineChartView(data: data, numberOfColumns: data.count, selectedDataPoint: $selectedCandle) { + LinearGradient( + gradient: Gradient(colors: [Color(.braveBlurpleTint).opacity(colourScheme == .dark ? 0.5 : 0.2), .clear]), + startPoint: .top, + endPoint: .bottom + ) + .shimmer(assetDetailStore.isLoadingChart) } + .chartAccessibility( + title: String.localizedStringWithFormat( + Strings.Wallet.assetDetailSubtitle, + assetDetailStore.assetDetailToken.name, + assetDetailStore.assetDetailToken.symbol), + dataPoints: data + ) + .disabled(data.isEmpty) + .frame(height: 128) + .padding(.horizontal, -12) + .animation(.default, value: data) } } var body: some View { - VStack(alignment: assetDetailStore.assetDetailToken.isFungibleToken ? .center : .leading, spacing: 0) { - if assetDetailStore.assetDetailToken.isFungibleToken { - VStack(alignment: .leading) { - if sizeCategory.isAccessibilityCategory { - VStack(alignment: .leading) { - if horizontalSizeClass == .regular { - DateRangeView(selectedRange: $assetDetailStore.timeframe) - .padding(6) - .overlay( - RoundedRectangle(cornerRadius: 8, style: .continuous) - .strokeBorder(Color(.secondaryButtonTint)) - ) - } - HStack { - tokenImageNameAndNetwork - .transaction { transaction in - transaction.animation = nil - transaction.disablesAnimations = true - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } else { - HStack { - tokenImageNameAndNetwork - .transaction { transaction in - transaction.animation = nil - transaction.disablesAnimations = true - } - if horizontalSizeClass == .regular { - Spacer() - DateRangeView(selectedRange: $assetDetailStore.timeframe) - .padding(6) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder(Color(.secondaryButtonTint)) - ) - } - } - } - Text( - String.localizedStringWithFormat( - Strings.Wallet.assetDetailSubtitle, - assetDetailStore.assetDetailToken.name, - assetDetailStore.assetDetailToken.symbol) - ) - .font(.footnote) - .foregroundColor(Color(.secondaryBraveLabel)) - VStack(alignment: .leading) { - HStack { - Group { - if let selectedCandle = selectedCandle, - let formattedString = assetDetailStore.currencyFormatter.string(from: NSNumber(value: selectedCandle.value)) { - Text(formattedString) - } else { - Text(assetDetailStore.price) - } - } - .font(.title2.bold()) - deltaText - Spacer() - } - Text(assetDetailStore.btcRatio) - .font(.footnote) - .foregroundColor(Color(.secondaryBraveLabel)) - } - .redacted(reason: assetDetailStore.isInitialState ? .placeholder : []) - .shimmer(assetDetailStore.isLoadingPrice) - let data = assetDetailStore.priceHistory.isEmpty ? emptyData : assetDetailStore.priceHistory - LineChartView(data: data, numberOfColumns: data.count, selectedDataPoint: $selectedCandle) { - LinearGradient( - gradient: Gradient(colors: [Color(.braveBlurpleTint).opacity(colourScheme == .dark ? 0.5 : 0.2), .clear]), - startPoint: .top, - endPoint: .bottom - ) - .shimmer(assetDetailStore.isLoadingChart) - } - .chartAccessibility( - title: String.localizedStringWithFormat( - Strings.Wallet.assetDetailSubtitle, - assetDetailStore.assetDetailToken.name, - assetDetailStore.assetDetailToken.symbol), - dataPoints: data - ) - .disabled(data.isEmpty) - .frame(height: 128) - .padding(.horizontal, -16) - .animation(.default, value: data) - if horizontalSizeClass == .compact { - DateRangeView(selectedRange: $assetDetailStore.timeframe) - } - } - .padding(16) - } else { - HStack { - AssetIconView(token: assetDetailStore.assetDetailToken, network: networkStore.defaultSelectedChain) - Text(assetDetailStore.assetDetailToken.nftTokenTitle) - .fixedSize(horizontal: false, vertical: true) - .font(.title3.weight(.semibold)) - Spacer() - } - .padding(16) - } - if assetDetailStore.assetDetailToken.isFungibleToken { - Divider() - .padding(.bottom) - } - actionButtonsContainer - .padding(.horizontal, assetDetailStore.assetDetailToken.isFungibleToken ? 0 : 16) - .transaction { transaction in - transaction.animation = nil - transaction.disablesAnimations = true - } + VStack(spacing: 0) { + tokenInfoView + .padding(.bottom, 8) + + lineChart } + .padding() + .frame(maxWidth: .infinity) + .background(Color(braveSystemName: .containerBackground)) } } @@ -282,10 +163,7 @@ struct CurrencyDetailHeaderView_Previews: PreviewProvider { AssetDetailHeaderView( assetDetailStore: .previewStore, keyringStore: .previewStore, - networkStore: .previewStore, - buySendSwapDestination: .constant(nil), - isShowingBridgeAlert: .constant(false), - onAccountCreationNeeded: { _ in } + networkStore: .previewStore ) .padding(.vertical) .previewLayout(.sizeThatFits) @@ -294,9 +172,3 @@ struct CurrencyDetailHeaderView_Previews: PreviewProvider { } } #endif - -private extension BraveWallet.BlockchainToken { - var isFungibleToken: Bool { - return !isErc721 && !isNft - } -} diff --git a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift index 58a0088d43e..1bdfa2b1be8 100644 --- a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift +++ b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift @@ -20,189 +20,298 @@ struct AssetDetailView: View { @ObservedObject var networkStore: NetworkStore @State private var tableInset: CGFloat = -16.0 - @State private var isShowingAddAccount: Bool = false @State private var transactionDetails: TransactionDetailsStore? @State private var isShowingAuroraBridgeAlert: Bool = false @State private var isPresentingAddAccount: Bool = false @State private var isPresentingAddAccountConfirmation: Bool = false @State private var savedBSSDestination: BuySendSwapDestination? + @State private var isShowingMoreActionSheet: Bool = false @Environment(\.sizeCategory) private var sizeCategory /// Reference to the collection view used to back the `List` on iOS 16+ @State private var collectionViewRef: WeakRef? @Environment(\.buySendSwapDestination) - private var buySendSwapDestination: Binding + @Binding private var buySendSwapDestination: BuySendSwapDestination? @Environment(\.openURL) private var openWalletURL @ObservedObject private var isShowingBalances = Preferences.Wallet.isShowingBalances - - @ViewBuilder private var accountsBalanceView: some View { - Section( - header: WalletListHeaderView(title: Text(Strings.Wallet.accountsPageTitle)), - footer: Button(action: { - isShowingAddAccount = true - }) { - Text(Strings.Wallet.addAccountTitle) - } - .listRowInsets(.zero) - .buttonStyle(BraveOutlineButtonStyle(size: .small)) - .padding(.vertical, 8) - ) { - Group { - if assetDetailStore.accounts.isEmpty { - Text(Strings.Wallet.noAccounts) + + @State private var selectedContent: AssetDetailSegmentedControl.Item = .accounts + /// Query displayed in the search bar above the transactions. + @State private var query: String = "" + @State private var isDoNotShowCheckboxChecked: Bool = false + + private var accountsBalanceHeader: some View { + VStack(spacing: 8) { + HStack { + Text(Strings.Wallet.accountsPageTitle) + .foregroundColor(Color(braveSystemName: .textPrimary)) + .font(.title3.weight(.semibold)) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + if assetDetailStore.isLoadingAccountBalances { + Text(Strings.Wallet.totalBalance) .redacted(reason: assetDetailStore.isLoadingAccountBalances ? .placeholder : []) .shimmer(assetDetailStore.isLoadingAccountBalances) - .font(.footnote) } else { - ForEach(assetDetailStore.accounts) { viewModel in - HStack { - AddressView(address: viewModel.account.address) { - AccountView(address: viewModel.account.address, name: viewModel.account.name) - } - let showFiatPlaceholder = viewModel.fiatBalance.isEmpty && assetDetailStore.isLoadingPrice - let showBalancePlaceholder = viewModel.balance.isEmpty && assetDetailStore.isLoadingAccountBalances - if assetDetailStore.assetDetailToken.isNft || assetDetailStore.assetDetailToken.isErc721 { - Text(showBalancePlaceholder ? "0 \(assetDetailStore.assetDetailToken.symbol)" : "\(viewModel.balance) \(assetDetailStore.assetDetailToken.symbol)") - .redacted(reason: showBalancePlaceholder ? .placeholder : []) - .shimmer(assetDetailStore.isLoadingAccountBalances) - .font(.footnote) - .foregroundColor(Color(.secondaryBraveLabel)) - } else { - Group { - if isShowingBalances.value { - VStack(alignment: .trailing) { - Text(showFiatPlaceholder ? "$0.00" : viewModel.fiatBalance) - .redacted(reason: showFiatPlaceholder ? .placeholder : []) - .shimmer(assetDetailStore.isLoadingPrice) - Text(showBalancePlaceholder ? "0.0000 \(assetDetailStore.assetDetailToken.symbol)" : "\(viewModel.balance) \(assetDetailStore.assetDetailToken.symbol)") - .redacted(reason: showBalancePlaceholder ? .placeholder : []) - .shimmer(assetDetailStore.isLoadingAccountBalances) - } - } else { - Text("****") - } - } - .font(.footnote) - .foregroundColor(Color(.secondaryBraveLabel)) - } - } - } + Text("\(assetDetailStore.totalBalance) \(assetDetailStore.assetDetailToken.symbol)") + .foregroundColor(Color(.braveLabel)) } } - .listRowBackground(Color(.secondaryBraveGroupedBackground)) + + DividerLine() } } - - @ViewBuilder private var transactionsView: some View { - Section( - header: WalletListHeaderView(title: Text(Strings.Wallet.transactionsTitle)) - ) { + + private func accontBalanceRow(_ viewModel: AccountAssetViewModel) -> some View { + HStack { + AddressView(address: viewModel.account.address) { + AccountView(address: viewModel.account.address, name: viewModel.account.name) + } + let showFiatPlaceholder = viewModel.fiatBalance.isEmpty && assetDetailStore.isLoadingPrice + let showBalancePlaceholder = viewModel.balance.isEmpty && assetDetailStore.isLoadingAccountBalances Group { - if assetDetailStore.transactionSummaries.isEmpty { - Text(Strings.Wallet.noTransactions) - .font(.footnote) + if isShowingBalances.value { + VStack(alignment: .trailing) { + Text(showBalancePlaceholder ? "0.0000 \(assetDetailStore.assetDetailToken.symbol)" : "\(viewModel.balance) \(assetDetailStore.assetDetailToken.symbol)") + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) + .redacted(reason: showBalancePlaceholder ? .placeholder : []) + .shimmer(assetDetailStore.isLoadingAccountBalances) + Text(showFiatPlaceholder ? "$0.00" : viewModel.fiatBalance) + .font(.footnote) + .foregroundColor(Color(.braveLabel)) + .redacted(reason: showFiatPlaceholder ? .placeholder : []) + .shimmer(assetDetailStore.isLoadingPrice) + } } else { - ForEach(assetDetailStore.transactionSummaries) { txSummary in - Button(action: { - self.transactionDetails = assetDetailStore.transactionDetailsStore(for: txSummary.txInfo) - }) { - TransactionSummaryView(summary: txSummary, displayAccountCreator: true) - } - .contextMenu { - if !txSummary.txHash.isEmpty { - Button(action: { - if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == txSummary.txInfo.chainId }), - let url = txNetwork.txBlockExplorerLink(txHash: txSummary.txHash, for: txNetwork.coin) { - openWalletURL(url) - } - }) { - Label(Strings.Wallet.viewOnBlockExplorer, systemImage: "arrow.up.forward.square") - } - } - } + Text("****") + } + } + .font(.footnote) + .foregroundColor(Color(.secondaryBraveLabel)) + } + } + + @ViewBuilder private var accountsBalanceView: some View { + VStack { + if assetDetailStore.accounts.isEmpty { + emptyAccountState + } else { + accountsBalanceHeader + + ForEach(assetDetailStore.accounts) { viewModel in + accontBalanceRow(viewModel) + } + } + } + } + + struct CoinMarketInfo: Identifiable { + let title: String + let value: String + var id: String { title } + } + + @ViewBuilder private func coinMarketInfoView(_ coinMarket: BraveWallet.CoinMarket) -> some View { + VStack(spacing: 16) { + VStack(spacing: 12) { + Text(Strings.Wallet.coinMarketInformation) + .foregroundColor(Color(braveSystemName: .textPrimary)) + .font(.title3.weight(.semibold)) + .frame(maxWidth: .infinity, alignment: .leading) + + DividerLine() + } + let grids = [GridItem(.adaptive(minimum: 160), spacing: 8, alignment: .top)] + let info: [CoinMarketInfo] = { + let computedMarketCap = assetDetailStore.currencyFormatter.string(from: NSNumber(value: BraveWallet.CoinMarket.abbreviateToBillion(input: coinMarket.marketCap))) ?? "" + let computedTotalVolume = assetDetailStore.currencyFormatter.string(from: NSNumber(value: BraveWallet.CoinMarket.abbreviateToBillion(input: coinMarket.totalVolume))) ?? "" + return [.init(title: Strings.Wallet.coinMarketRank, value: "#\(coinMarket.marketCapRank)"), .init(title: Strings.Wallet.coinMarketMarketCap, value: "\(computedMarketCap)B"), .init(title: Strings.Wallet.coinMarket24HVolume, value: "\(computedTotalVolume)B")] + }() + LazyVGrid(columns: grids) { + ForEach(info) { item in + VStack(spacing: 4) { + Text(item.value) + .font(.body.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) + Text(item.title) + .font(.caption2) + .foregroundColor(Color(.braveLabel)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Color(braveSystemName: .dividerSubtle), lineWidth: 1) } } } - .listRowBackground(Color(.secondaryBraveGroupedBackground)) } + .padding(.horizontal) } - private func coinMarketInfoView(_ coinMarket: BraveWallet.CoinMarket) -> some View { - Section { - HStack { - VStack(spacing: 10) { - Text("\(coinMarket.marketCapRank)") - .font(.title3.weight(.semibold)) - Text(Strings.Wallet.coinMarketRank) - .font(.footnote) + private var emptyTransactionState: some View { + VStack(spacing: 10) { + Image("transaction-empty", bundle: .module) + .aspectRatio(contentMode: .fit) + Text(Strings.Wallet.noTransactions) + .font(.headline) + .foregroundColor(Color(WalletV2Design.textPrimary)) + Text(Strings.Wallet.activityPageEmptyDescription) + .font(.footnote) + .foregroundColor(Color(WalletV2Design.textSecondary)) + } + .multilineTextAlignment(.center) + .padding(.vertical) + } + + private var emptyAccountState: some View { + VStack(spacing: 10) { + Image("account-empty", bundle: .module) + .aspectRatio(contentMode: .fit) + Text(Strings.Wallet.noAccounts) + .font(.headline) + .foregroundColor(Color(WalletV2Design.textPrimary)) + Text(Strings.Wallet.noAccountDescription) + .font(.footnote) + .foregroundColor(Color(WalletV2Design.textSecondary)) + } + .multilineTextAlignment(.center) + .padding(.vertical) + } + + @ViewBuilder var actionButtonsContainer: some View { + HStack(alignment: .top, spacing: 40) { + if assetDetailStore.isBuySupported { + PortfolioHeaderButton(style: .buy) { + let destination = BuySendSwapDestination( + kind: .buy, + initialToken: assetDetailStore.assetDetailToken + ) + if assetDetailStore.accounts.isEmpty { + onAccountCreationNeeded(destination) + } else { + buySendSwapDestination = destination + } } - Spacer() - VStack(spacing: 10) { - let computed = assetDetailStore.currencyFormatter.string(from: NSNumber(value: BraveWallet.CoinMarket.abbreviateToBillion(input: coinMarket.totalVolume))) ?? "" - Text("\(computed)B") - .font(.title3.weight(.semibold)) - Text(Strings.Wallet.coinMarket24HVolume) - .font(.footnote) + } + if assetDetailStore.isSendSupported { + PortfolioHeaderButton(style: .send) { + let destination = BuySendSwapDestination( + kind: .send, + initialToken: assetDetailStore.assetDetailToken + ) + if assetDetailStore.accounts.isEmpty { + onAccountCreationNeeded(destination) + } else { + buySendSwapDestination = destination + } } - Spacer() - VStack(spacing: 10) { - let computed = assetDetailStore.currencyFormatter.string(from: NSNumber(value: BraveWallet.CoinMarket.abbreviateToBillion(input: coinMarket.marketCap))) ?? "" - Text("\(computed)B") - .font(.title3.weight(.semibold)) - Text(Strings.Wallet.coinMarketMarketCap) - .font(.footnote) + } + if assetDetailStore.isSwapSupported { + PortfolioHeaderButton(style: .swap) { + let destination = BuySendSwapDestination( + kind: .swap, + initialToken: assetDetailStore.assetDetailToken + ) + if assetDetailStore.accounts.isEmpty { + onAccountCreationNeeded(destination) + } else { + buySendSwapDestination = destination + } + } + } + if case let .blockchainToken(token) = assetDetailStore.assetDetailType, token.isAuroraSupportedToken { + PortfolioHeaderButton(style: .more) { + isShowingMoreActionSheet = true } } - .foregroundColor(Color(.braveLabel)) - .listRowBackground(Color(.secondaryBraveGroupedBackground)) - } header: { - Text(Strings.Wallet.coinMarketInformation) + } + .padding(.horizontal, 16) + .transaction { transaction in + transaction.animation = nil + transaction.disablesAnimations = true } } - var body: some View { - List { - Section( - header: AssetDetailHeaderView( - assetDetailStore: assetDetailStore, - keyringStore: keyringStore, - networkStore: networkStore, - buySendSwapDestination: buySendSwapDestination, - isShowingBridgeAlert: $isShowingAuroraBridgeAlert, - onAccountCreationNeeded: { savedDestination in - isPresentingAddAccountConfirmation = true - savedBSSDestination = savedDestination - } - ) - .resetListHeaderStyle() - .padding(.horizontal, tableInset) // inset grouped layout margins workaround - ) { + @ViewBuilder private var tokenContentContainer: some View { + VStack(spacing: 0) { + actionButtonsContainer + .padding(.bottom, 40) + + AssetDetailSegmentedControl(selected: $selectedContent) + .padding(.horizontal) + if selectedContent == .accounts { + accountsBalanceView + .padding(.horizontal) + .padding(.top, 16) + } else { + if assetDetailStore.transactionSections.isEmpty { + emptyTransactionState + } else { + TransactionsListView( + transactionSections: assetDetailStore.transactionSections, + query: $query, + showFilter: false, + filtersButtonTapped: {}, + transactionTapped: { tx in + self.transactionDetails = assetDetailStore.transactionDetailsStore(for: tx) + } + ) + } } + } + } + + private var assetDetailContentView: some View { + LazyVStack { switch assetDetailStore.assetDetailType { case .blockchainToken(_): - accountsBalanceView - transactionsView - case .coinMarket(let coinMarket): - coinMarketInfoView(coinMarket) - } - if !assetDetailStore.assetDetailToken.isNft && !assetDetailStore.assetDetailToken.isErc721 { - Section { - EmptyView() - } header: { + tokenContentContainer + .padding(.bottom, 12) + + if (selectedContent == .accounts && !assetDetailStore.accounts.isEmpty) || (selectedContent == .transactions && !assetDetailStore.transactionSections.isEmpty) { Text(Strings.Wallet.coinGeckoDisclaimer) .multilineTextAlignment(.center) .font(.footnote) .foregroundColor(Color(.secondaryBraveLabel)) .frame(maxWidth: .infinity) - .listRowBackground(Color(.braveGroupedBackground)) - .resetListHeaderStyle(insets: nil) + .padding(.bottom, 12) } + case .coinMarket(let coinMarket): + coinMarketInfoView(coinMarket) + .padding(.bottom, 20) + Text(Strings.Wallet.coinGeckoDisclaimer) + .multilineTextAlignment(.center) + .font(.footnote) + .foregroundColor(Color(.secondaryBraveLabel)) + .frame(maxWidth: .infinity) + .padding(.bottom, 12) } } - .listStyle(InsetGroupedListStyle()) - .listBackgroundColor(Color(UIColor.braveGroupedBackground)) + .padding(.vertical) + .background(Color(braveSystemName: .containerBackground)) + } + + var body: some View { + ScrollView { + VStack(spacing: 0) { + AssetDetailHeaderView( + assetDetailStore: assetDetailStore, + keyringStore: keyringStore, + networkStore: networkStore + ) + + assetDetailContentView + } + } + .background( + Color(braveSystemName: .containerBackground).edgesIgnoringSafeArea(.all) + ) .navigationTitle(assetDetailStore.assetDetailToken.name) .navigationBarTitleDisplayMode(.inline) .onAppear { @@ -220,18 +329,26 @@ struct AssetDetailView: View { ) { (collectionView: UICollectionView) in self.collectionViewRef = .init(collectionView) } - .background( - Color.clear - .sheet(isPresented: $isShowingAddAccount) { - NavigationView { - AddAccountView( - keyringStore: keyringStore, - networkStore: networkStore - ) - } - .navigationViewStyle(StackNavigationViewStyle()) - } - ) + .actionSheet(isPresented: $isShowingMoreActionSheet) { + ActionSheet( + title: Text("\(assetDetailStore.assetDetailToken.name) (\(assetDetailStore.assetDetailToken.symbol))"), + buttons: [ + .cancel(), + .default( + Text(Strings.Wallet.auroraBridgeButtonTitle), + action: { + if Preferences.Wallet.showAuroraPopup.value { + isShowingAuroraBridgeAlert = true + } else { + if let link = WalletConstants.auroraBridgeLink { + openWalletURL(link) + } + } + } + ) + ] + ) + } .background( Color.clear .sheet( @@ -259,34 +376,35 @@ struct AssetDetailView: View { primaryButton: .init( title: Strings.Wallet.auroraBridgeButtonTitle, action: { _ in + if isDoNotShowCheckboxChecked { + Preferences.Wallet.showAuroraPopup.value = false + } isShowingAuroraBridgeAlert = false if let link = WalletConstants.auroraBridgeLink { openWalletURL(link) } } ), + secondaryButton: .init( + title: Strings.CancelString, + action: { _ in + if isDoNotShowCheckboxChecked { + Preferences.Wallet.showAuroraPopup.value = false + } + isShowingAuroraBridgeAlert = false + } + ), showCloseButton: false, content: { - VStack(spacing: 10) { + VStack(spacing: 16) { Text(Strings.Wallet.auroraBridgeAlertTitle) - .font(.headline.weight(.bold)) + .font(.body.weight(.medium)) + .foregroundColor(Color(.bravePrimary)) .multilineTextAlignment(.center) - .padding(.vertical) Text(Strings.Wallet.auroraBridgeAlertDescription) .multilineTextAlignment(.center) - .font(.subheadline) - } - }, - footer: { - VStack(spacing: 8) { - Button(action: { - isShowingAuroraBridgeAlert = false - Preferences.Wallet.showAuroraPopup.value = false - }) { - Text(Strings.Wallet.auroraPopupDontShowAgain) - .foregroundColor(Color(.braveLabel)) - .font(.callout.weight(.semibold)) - } + .font(.footnote) + .foregroundColor(Color(.braveLabel)) Button { isShowingAuroraBridgeAlert = false if let link = WalletConstants.auroraBridgeOverviewLink { @@ -296,7 +414,8 @@ struct AssetDetailView: View { Text(Strings.Wallet.auroraBridgeLearnMore) .multilineTextAlignment(.center) .foregroundColor(Color(.braveBlurpleTint)) - .font(.subheadline) + .font(.footnote.weight(.semibold)) + .padding(8) } Button { isShowingAuroraBridgeAlert = false @@ -307,10 +426,18 @@ struct AssetDetailView: View { Text(Strings.Wallet.auroraBridgeRisk) .multilineTextAlignment(.center) .foregroundColor(Color(.braveBlurpleTint)) + .font(.footnote.weight(.semibold)) + .padding(8) + } + HStack(alignment: .top, spacing: 8) { + LegalCheckbox(isChecked: $isDoNotShowCheckboxChecked) + Text(Strings.Wallet.auroraPopupDontShowAgain) + .foregroundColor(Color(.bravePrimary)) .font(.subheadline) + Spacer() } } - .padding(.top, 16) + .padding(.bottom, 16) } ) ) @@ -319,25 +446,11 @@ struct AssetDetailView: View { isShowingAuroraBridgeAlert = false } } - .addAccount( - keyringStore: keyringStore, - networkStore: networkStore, - accountNetwork: networkStore.network(for: assetDetailStore.assetDetailToken), - isShowingConfirmation: $isPresentingAddAccountConfirmation, - isShowingAddAccount: $isPresentingAddAccount, - onConfirmAddAccount: { isPresentingAddAccount = true }, - onCancelAddAccount: nil, - onAddAccountDismissed: { - Task { @MainActor in - if await assetDetailStore.handleDismissAddAccount() { - if let savedBSSDestination { - buySendSwapDestination.wrappedValue = savedBSSDestination - self.savedBSSDestination = nil - } - } - } - } - ) + } + + private func onAccountCreationNeeded(_ destination: BuySendSwapDestination) { + isPresentingAddAccountConfirmation = true + savedBSSDestination = destination } } @@ -356,3 +469,29 @@ struct CurrencyDetailView_Previews: PreviewProvider { } } #endif + +struct AssetDetailSegmentedControl: View { + + enum Item: Int, Equatable, CaseIterable, Identifiable, WalletSegmentedControlItem { + case accounts + case transactions + + var title: String { + switch self { + case .accounts: return Strings.Wallet.accountsPageTitle + case .transactions: return Strings.Wallet.transactionsTitle + } + } + + var id: Int { rawValue } + } + + @Binding var selected: Item + + var body: some View { + WalletSegmentedControl( + items: Item.allCases, + selected: $selected + ) + } +} diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift index 7ce92d72f45..59c81f6d092 100644 --- a/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift @@ -129,44 +129,9 @@ struct PortfolioHeaderView: View { .padding(.horizontal, 30) } - private var timeframeSelector: some View { - Menu(content: { - ForEach(BraveWallet.AssetPriceTimeframe.allCases, id: \.self) { range in - Button(action: { selectedDateRange = range }) { - HStack { - Image(braveSystemName: "leo.check.normal") - .resizable() - .aspectRatio(contentMode: .fit) - .hidden(isHidden: selectedDateRange != range) - Text(verbatim: range.accessibilityLabel) - } - .tag(range) - } - } - }, label: { - HStack(spacing: 4) { - Text(verbatim: selectedDateRange.accessibilityLabel) - .font(.footnote.weight(.semibold)) - Image(braveSystemName: "leo.carat.down") - } - .foregroundColor(Color(braveSystemName: .textInteractive)) - .padding(.vertical, 6) - .padding(.horizontal, 12) - .padding(.trailing, -4) // whitespace on `leo.carat.down` symbol - .background( - Capsule() - .strokeBorder(Color(braveSystemName: .dividerInteractive), lineWidth: 1) - ) - }) - .transaction { transaction in - transaction.animation = nil - transaction.disablesAnimations = true - } - } - @ViewBuilder private var lineChart: some View { VStack(spacing: 0) { - timeframeSelector + TimeframeSelector(selectedDateRange: $selectedDateRange) let chartData = historicalBalances.isEmpty ? emptyBalanceData : historicalBalances LineChartView(data: chartData, numberOfColumns: chartData.count, selectedDataPoint: $selectedBalance) { LinearGradient( @@ -188,13 +153,18 @@ struct PortfolioHeaderView: View { } } -private struct PortfolioHeaderButton: View { +struct PortfolioHeaderButton: View { enum Style: String, Equatable { case buy, send, swap, more var label: String { - rawValue.capitalizeFirstLetter + switch self { + case .buy: return Strings.Wallet.buy + case .send: return Strings.Wallet.send + case .swap: return Strings.Wallet.swap + case .more: return Strings.Wallet.more + } } var iconName: String { @@ -229,3 +199,42 @@ private struct PortfolioHeaderButton: View { } } } + +struct TimeframeSelector: View { + @Binding var selectedDateRange: BraveWallet.AssetPriceTimeframe + + var body: some View { + Menu(content: { + ForEach(BraveWallet.AssetPriceTimeframe.allCases, id: \.self) { range in + Button(action: { selectedDateRange = range }) { + HStack { + Image(braveSystemName: "leo.check.normal") + .resizable() + .aspectRatio(contentMode: .fit) + .hidden(isHidden: selectedDateRange != range) + Text(verbatim: range.accessibilityLabel) + } + .tag(range) + } + } + }, label: { + HStack(spacing: 4) { + Text(verbatim: selectedDateRange.accessibilityLabel) + .font(.footnote.weight(.semibold)) + Image(braveSystemName: "leo.carat.down") + } + .foregroundColor(Color(braveSystemName: .textInteractive)) + .padding(.vertical, 6) + .padding(.horizontal, 12) + .padding(.trailing, -4) // whitespace on `leo.carat.down` symbol + .background( + Capsule() + .strokeBorder(Color(braveSystemName: .dividerInteractive), lineWidth: 1) + ) + }) + .transaction { transaction in + transaction.animation = nil + transaction.disablesAnimations = true + } + } +} diff --git a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift index 8a73c3b90e8..3ab92353331 100644 --- a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift @@ -39,7 +39,6 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { @Published private(set) var price: String = "$0.0000" @Published private(set) var priceDelta: String = "0.00%" @Published private(set) var priceIsDown: Bool = false - @Published private(set) var btcRatio: String = "0.0000 BTC" @Published private(set) var priceHistory: [BraveWallet.AssetTimePrice] = [] @Published var timeframe: BraveWallet.AssetPriceTimeframe = .oneDay { didSet { @@ -50,7 +49,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { } @Published private(set) var isLoadingAccountBalances: Bool = false @Published private(set) var accounts: [AccountAssetViewModel] = [] - @Published private(set) var transactionSummaries: [TransactionSummary] = [] + @Published private(set) var transactionSections: [TransactionSection] = [] @Published private(set) var isBuySupported: Bool = false @Published private(set) var isSendSupported: Bool = false @Published private(set) var isSwapSupported: Bool = false @@ -67,6 +66,12 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { let currencyFormatter: NumberFormatter = .usdCurrencyFormatter + var totalBalance: Double { + accounts + .compactMap { Double($0.balance) } + .reduce(0, +) + } + private(set) var assetPriceValue: Double = 0.0 private let assetRatioService: BraveWalletAssetRatioService @@ -181,8 +186,12 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { $0.maximumFractionDigits = 2 } + private var updateTask: Task? + private var solEstimatedTxFeesCache: [String: UInt64] = [:] + private var assetPricesCache: [String: Double] = [:] public func update() { - Task { @MainActor in + updateTask?.cancel() + updateTask = Task { @MainActor in self.isLoadingPrice = true self.isLoadingChart = true @@ -203,35 +212,88 @@ class AssetDetailStore: ObservableObject, WalletObserverStore { AccountAssetViewModel(account: $0, decimalBalance: 0.0, balance: "", fiatBalance: "") } - if !token.isErc721 && !token.isNft { - // fetch prices for the asset - let (prices, btcRatio, priceHistory) = await fetchPriceInfo(for: token.assetRatioId) - self.btcRatio = btcRatio - self.priceHistory = priceHistory - self.isLoadingPrice = false - self.isInitialState = false - self.isLoadingChart = false - - if let assetPrice = prices.first(where: { $0.toAsset.caseInsensitiveCompare(self.currencyFormatter.currencyCode) == .orderedSame }), - let value = Double(assetPrice.price) { - self.assetPriceValue = value - self.price = self.currencyFormatter.string(from: NSNumber(value: value)) ?? "" - if let deltaValue = Double(assetPrice.assetTimeframeChange) { - self.priceIsDown = deltaValue < 0 - self.priceDelta = self.percentFormatter.string(from: NSNumber(value: deltaValue / 100.0)) ?? "" - } - for index in 0.. 0 ? "1" : "0" - } else { - accountAssetViewModels[index].balance = String(format: "%.4f", tokenBalance.balance ?? 0.0) - accountAssetViewModels[index].fiatBalance = self.currencyFormatter.string(from: NSNumber(value: accountAssetViewModels[index].decimalBalance * assetPriceValue)) ?? "" - } + accountAssetViewModels[index].balance = String(format: "%.4f", tokenBalance.balance ?? 0.0) + accountAssetViewModels[index].fiatBalance = self.currencyFormatter.string(from: NSNumber(value: accountAssetViewModels[index].decimalBalance * assetPriceValue)) ?? "" } } self.isLoadingAccountBalances = false - return accountAssetViewModels + return accountAssetViewModels.filter { $0.decimalBalance > 0 } } - @MainActor private func fetchTransactionSummarys( - accounts: [BraveWallet.AccountInfo], + private func buildTransactionSections( + transactions: [BraveWallet.TransactionInfo], network: BraveWallet.NetworkInfo, - assetRatios: [String: Double] - ) async -> [TransactionSummary] { - guard case let .blockchainToken(token) = assetDetailType - else { return [] } - 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 accounts { - group.addTask { @MainActor in - await self.txService.allTransactionInfo( - network.coin, - chainId: network.chainId, - from: account.accountId + accountInfos: [BraveWallet.AccountInfo], + userAssets: [BraveWallet.BlockchainToken], + allTokens: [BraveWallet.BlockchainToken], + assetRatios: [String: Double], + nftMetadata: [String: NFTMetadata], + solEstimatedTxFees: [String: UInt64] + ) -> [TransactionSection] { + // Group transactions by day (only compare day/month/year) + let transactionsGroupedByDate = Dictionary(grouping: transactions) { transaction in + let dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: transaction.createdTime) + return Calendar.current.date(from: dateComponents) ?? transaction.createdTime + } + // Map to 1 `TransactionSection` per date + return transactionsGroupedByDate.keys.sorted(by: { $0 > $1 }).compactMap { date in + let transactions = transactionsGroupedByDate[date] ?? [] + guard !transactions.isEmpty else { return nil } + let parsedTransactions: [ParsedTransaction] = transactions + .sorted(by: { $0.createdTime > $1.createdTime }) + .compactMap { transaction in + return TransactionParser.parseTransaction( + transaction: transaction, + network: network, + accountInfos: accountInfos, + userAssets: userAssets, + allTokens: allTokens + tokenInfoCache, + assetRatios: assetRatios, + nftMetadata: nftMetadata, + solEstimatedTxFee: solEstimatedTxFees[transaction.id], + currencyFormatter: currencyFormatter, + decimalFormatStyle: .decimals(precision: 4) ) } - } - return await group.reduce([BraveWallet.TransactionInfo](), { partialResult, prior in - return partialResult + prior - }) + return TransactionSection( + date: date, + transactions: parsedTransactions + ) } - var solEstimatedTxFees: [String: UInt64] = [:] - switch token.coin { - case .eth: - let ethTransactions = allTransactions.filter { $0.coin == .eth } - if !ethTransactions.isEmpty { // we can only fetch unknown Ethereum tokens - let unknownTokenInfo = ethTransactions.unknownTokenContractAddressChainIdPairs( - knownTokens: userAssets + allTokens + tokenInfoCache - ) - updateUnknownTokens(for: unknownTokenInfo) - } - case .sol: - solEstimatedTxFees = await solTxManagerProxy.estimatedTxFees(for: allTransactions) - default: - break + } + + @MainActor private func updateSolEstimatedTxFeesCache(_ solTransactions: [BraveWallet.TransactionInfo]) async { + let fees = await solTxManagerProxy.estimatedTxFees(for: solTransactions) + for (key, value) in fees { // update cached values + self.solEstimatedTxFeesCache[key] = value + } + } + + @MainActor private func updateAssetPricesCache(assetRatioIds: [String]) async { + let prices = await assetRatioService.fetchPrices( + for: assetRatioIds, + toAssets: [currencyFormatter.currencyCode], + timeframe: .oneDay + ).compactMapValues { Double($0) } + for (key, value) in prices { // update cached values + self.assetPricesCache[key] = value } - return allTransactions - .filter { tx in - switch tx.txType { - case .erc20Approve, .erc20Transfer: - guard let tokenContractAddress = tx.txDataUnion.ethTxData1559?.baseData.to else { - return false - } - return tokenContractAddress.caseInsensitiveCompare(token.contractAddress) == .orderedSame - case .ethSend, .ethSwap, .other: - return network.symbol.caseInsensitiveCompare(token.symbol) == .orderedSame - case .erc721TransferFrom, .erc721SafeTransferFrom: - guard let tokenContractAddress = tx.txDataUnion.ethTxData1559?.baseData.to else { return false } - return tokenContractAddress.caseInsensitiveCompare(token.contractAddress) == .orderedSame - case .solanaSystemTransfer: - return network.symbol.caseInsensitiveCompare(token.symbol) == .orderedSame - case .solanaSplTokenTransfer, .solanaSplTokenTransferWithAssociatedTokenAccountCreation: - guard let tokenContractAddress = tx.txDataUnion.solanaTxData?.splTokenMintAddress else { - return false - } - return tokenContractAddress.caseInsensitiveCompare(token.contractAddress) == .orderedSame - case .erc1155SafeTransferFrom, .solanaDappSignTransaction, .solanaDappSignAndSendTransaction, .solanaSwap: - return false - case .ethFilForwarderTransfer: - return false - @unknown default: - return false - } - } - .sorted(by: { $0.createdTime > $1.createdTime }) - .map { transaction in - TransactionParser.transactionSummary( - from: transaction, - network: network, - accountInfos: accounts, - userAssets: userAssets, - allTokens: allTokens + tokenInfoCache, - assetRatios: assetRatios, - nftMetadata: [:], - solEstimatedTxFee: solEstimatedTxFees[transaction.id], - currencyFormatter: self.currencyFormatter - ) - } } private var transactionDetailsStore: TransactionDetailsStore? diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift index daaac3af6db..daec4ed00cc 100644 --- a/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift +++ b/Sources/BraveWallet/Crypto/Transactions/TransactionsListView.swift @@ -21,6 +21,8 @@ struct TransactionsListView: View { let transactionSections: [TransactionSection] /// Query displayed in the search bar above the transactions. @Binding var query: String + /// Whether to display the filter button + var showFilter: Bool = true /// Called when the filters button beside the search bar is tapped/ let filtersButtonTapped: () -> Void /// Called when a transaction is tapped. @@ -87,10 +89,14 @@ struct TransactionsListView: View { VStack(spacing: 0) { HStack(spacing: 10) { SearchBar(text: $query, placeholder: Strings.Wallet.search) - WalletIconButton(braveSystemName: "leo.filter.settings", action: filtersButtonTapped) + if showFilter { + WalletIconButton(braveSystemName: "leo.filter.settings", action: filtersButtonTapped) + .padding(.trailing, 8) + } } .padding(.vertical, 8) Divider() + .padding(.horizontal, 8) } .padding(.horizontal, 8) .frame(maxWidth: .infinity) diff --git a/Sources/BraveWallet/WalletPromptView.swift b/Sources/BraveWallet/WalletPromptView.swift index 2d207209ab9..f70d794c0d1 100644 --- a/Sources/BraveWallet/WalletPromptView.swift +++ b/Sources/BraveWallet/WalletPromptView.swift @@ -50,7 +50,7 @@ struct WalletPromptContentView: View where Content: View, Foote Button { secondaryButton.action(nil) } label: { Text(secondaryButton.title) .font(.footnote.weight(.semibold)) - .foregroundColor(Color(.bravePrimary)) + .foregroundColor(Color(.braveLabel)) .frame(maxWidth: .infinity) } } @@ -59,11 +59,13 @@ struct WalletPromptContentView: View where Content: View, Foote Button { secondaryButton.action(nil) } label: { Text(secondaryButton.title) .font(.footnote.weight(.semibold)) + .foregroundColor(Color(.braveLabel)) } .buttonStyle(BraveOutlineButtonStyle(size: .large)) Button { primaryButton.action(nil) } label: { Text(primaryButton.title) .font(.footnote.weight(.semibold)) + .foregroundColor(Color(.bravePrimary)) } .buttonStyle(BraveFilledButtonStyle(size: .large)) } diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index 1544733d0e3..ee3b37308ab 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -50,6 +50,13 @@ extension Strings { value: "Accounts", comment: "The title of the accounts page in the Crypto tab" ) + public static let totalBalance = NSLocalizedString( + "wallet.totalBalance", + tableName: "BraveWallet", + bundle: .module, + value: "Total Balance", + comment: "A title label that will display total balance of all none-zero balance accounts" + ) public static let selectedNetworkAccessibilityLabel = NSLocalizedString( "wallet.selectedNetwork", tableName: "BraveWallet", @@ -113,6 +120,13 @@ extension Strings { value: "No Accounts", comment: "The empty state displayed when the user has no accounts associated with a transaction or asset" ) + public static let noAccountDescription = NSLocalizedString( + "wallet.noAccounts", + tableName: "BraveWallet", + bundle: .module, + value: "Accounts with a balance will appear here.", + comment: "The empty state description displayed when the user has no accounts associated with a transaction or asset" + ) public static let noTransactions = NSLocalizedString( "wallet.noTransactions", tableName: "BraveWallet", @@ -505,6 +519,13 @@ extension Strings { value: "Swap tokens and assets.", comment: "The description of a swap button on the buy/send/swap modal" ) + public static let more = NSLocalizedString( + "wallet.more", + tableName: "BraveWallet", + bundle: .module, + value: "More", + comment: "A button title for user to open more option in asset details screen other than buy/send/swap." + ) public static let infoTitle = NSLocalizedString( "wallet.infoTitle", tableName: "BraveWallet", diff --git a/Tests/BraveWalletTests/AssetDetailStoreTests.swift b/Tests/BraveWalletTests/AssetDetailStoreTests.swift index 4c11fb63b6f..3cbf2ebeb79 100644 --- a/Tests/BraveWalletTests/AssetDetailStoreTests.swift +++ b/Tests/BraveWalletTests/AssetDetailStoreTests.swift @@ -17,7 +17,7 @@ class AssetDetailStoreTests: XCTestCase { let assetRatioService = BraveWallet.TestAssetRatioService() assetRatioService._price = { _, _, _, completion in - completion(true, [.init(fromAsset: BraveWallet.BlockchainToken.previewToken.tokenId, toAsset: "btc", price: "0.1", assetTimeframeChange: "1"), .init(fromAsset: BraveWallet.BlockchainToken.previewToken.tokenId, toAsset: "usd", price: "1", assetTimeframeChange: "1")]) + completion(true, [.init(fromAsset: BraveWallet.BlockchainToken.previewToken.tokenId, toAsset: "usd", price: "1", assetTimeframeChange: "1")]) } assetRatioService._priceHistory = { _, _, _, completion in completion(true, [.init(date: Date(), price: "0.99")]) @@ -98,7 +98,7 @@ class AssetDetailStoreTests: XCTestCase { ) let assetDetailException = expectation(description: "update-blockchainToken") - assetDetailException.expectedFulfillmentCount = 14 + assetDetailException.expectedFulfillmentCount = 13 store.$network .dropFirst() .sink { network in @@ -127,13 +127,6 @@ class AssetDetailStoreTests: XCTestCase { XCTAssertTrue($0) } .store(in: &cancellables) - store.$btcRatio - .dropFirst() - .sink { - defer { assetDetailException.fulfill() } - XCTAssertEqual($0, "0.1 BTC") - } - .store(in: &cancellables) store.$priceHistory .dropFirst() .sink { priceHistory in @@ -173,12 +166,18 @@ class AssetDetailStoreTests: XCTestCase { XCTAssertEqual(accounts[0].fiatBalance, formattedEthBalance) } .store(in: &cancellables) - store.$transactionSummaries + store.$transactionSections .dropFirst() - .sink { tx in + .collect(3) + .sink { updates in defer { assetDetailException.fulfill() } - XCTAssertEqual(tx.count, 1) - XCTAssertEqual(tx[0].txInfo.id, BraveWallet.TransactionInfo.previewConfirmedSend.id) + guard let txSections = updates.last else { + XCTFail("Unexpected transactionSections") + return + } + XCTAssertEqual(txSections.count, 1) + XCTAssertEqual(txSections.first!.transactions.count, 1) + XCTAssertEqual(txSections.first!.transactions.first!.transaction.id, BraveWallet.TransactionInfo.previewConfirmedSend.id) } .store(in: &cancellables) store.$isLoadingPrice @@ -292,7 +291,7 @@ class AssetDetailStoreTests: XCTestCase { ) let assetDetailBitcoinException = expectation(description: "update-coinMarket-bitcoin") - assetDetailBitcoinException.expectedFulfillmentCount = 11 + assetDetailBitcoinException.expectedFulfillmentCount = 10 store.$isBuySupported .dropFirst() .sink { @@ -314,13 +313,6 @@ class AssetDetailStoreTests: XCTestCase { XCTAssertFalse($0) } .store(in: &cancellables) - store.$btcRatio - .dropFirst() - .sink { - defer { assetDetailBitcoinException.fulfill() } - XCTAssertEqual($0, "1 BTC") - } - .store(in: &cancellables) store.$priceHistory .dropFirst() .sink { priceHistory in @@ -377,7 +369,7 @@ class AssetDetailStoreTests: XCTestCase { defer { XCTAssertNil(store.network) XCTAssertTrue(store.accounts.isEmpty) - XCTAssertTrue(store.transactionSummaries.isEmpty) + XCTAssertTrue(store.transactionSections.isEmpty) assetDetailBitcoinException.fulfill() } @@ -409,7 +401,7 @@ class AssetDetailStoreTests: XCTestCase { assetDetailType: .coinMarket(.mockCoinMarketEth) ) let assetDetailNonBitcoinException = expectation(description: "update-coinMarket-non-bitcoin") - assetDetailNonBitcoinException.expectedFulfillmentCount = 11 + assetDetailNonBitcoinException.expectedFulfillmentCount = 10 store.$isBuySupported .dropFirst() .sink { @@ -431,13 +423,6 @@ class AssetDetailStoreTests: XCTestCase { XCTAssertFalse($0) } .store(in: &cancellables) - store.$btcRatio - .dropFirst() - .sink { - defer { assetDetailNonBitcoinException.fulfill() } - XCTAssertEqual($0, "0.1 BTC") - } - .store(in: &cancellables) store.$priceHistory .dropFirst() .sink { priceHistory in @@ -466,7 +451,7 @@ class AssetDetailStoreTests: XCTestCase { defer { XCTAssertNil(store.network) XCTAssertTrue(store.accounts.isEmpty) - XCTAssertTrue(store.transactionSummaries.isEmpty) + XCTAssertTrue(store.transactionSections.isEmpty) assetDetailNonBitcoinException.fulfill() } @@ -500,7 +485,7 @@ class AssetDetailStoreTests: XCTestCase { defer { XCTAssertNil(store.network) XCTAssertTrue(store.accounts.isEmpty) - XCTAssertTrue(store.transactionSummaries.isEmpty) + XCTAssertTrue(store.transactionSections.isEmpty) assetDetailNonBitcoinException.fulfill() }