From c934dd9874c916004ac8b4fd1f5f83f75ded5f47 Mon Sep 17 00:00:00 2001 From: StephenHeaps <5314553+StephenHeaps@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:20:42 -0400 Subject: [PATCH] Fix #8072: Portfolio v2 (#8140) * Integrate Portfolio v2 UI * Remove floating Buy/Send/Swap button * Use reusable modifier for reading view size --- .../BraveWallet/Crypto/AssetIconView.swift | 4 +- .../BraveWallet/Crypto/CryptoPagesView.swift | 49 +- Sources/BraveWallet/Crypto/NFT/NFTView.swift | 37 +- .../Crypto/Portfolio/AssetButton.swift | 29 ++ .../Crypto/Portfolio/PortfolioAssetView.swift | 3 + .../Portfolio/PortfolioAssetsView.swift | 235 +++++++++ .../Portfolio/PortfolioHeaderView.swift | 220 +++++++++ .../Portfolio/PortfolioSegmentedControl.swift | 195 ++++++++ .../Crypto/Portfolio/PortfolioView.swift | 451 +++--------------- .../Crypto/Stores/PortfolioStore.swift | 28 ++ .../Crypto/WalletDisclosureGroup.swift | 44 +- Sources/BraveWallet/SwapButton.swift | 64 --- .../leo.coins.alt1.symbolset/Contents.json | 11 + .../Contents.json | 11 + .../Contents.json | 11 + .../leo.send.symbolset/Contents.json | 11 + 16 files changed, 881 insertions(+), 522 deletions(-) create mode 100644 Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift create mode 100644 Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift create mode 100644 Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift create mode 100644 Sources/BraveWallet/Crypto/Portfolio/PortfolioSegmentedControl.swift delete mode 100644 Sources/BraveWallet/SwapButton.swift create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.coins.alt1.symbolset/Contents.json create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.currency.exchange.symbolset/Contents.json create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.filter.settings.symbolset/Contents.json create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.send.symbolset/Contents.json diff --git a/Sources/BraveWallet/Crypto/AssetIconView.swift b/Sources/BraveWallet/Crypto/AssetIconView.swift index 0440ec662eb..48db9ebb91b 100644 --- a/Sources/BraveWallet/Crypto/AssetIconView.swift +++ b/Sources/BraveWallet/Crypto/AssetIconView.swift @@ -86,7 +86,7 @@ struct AssetIconView: View { .overlay( Circle() .stroke(lineWidth: 2) - .foregroundColor(.white) + .foregroundColor(Color(braveSystemName: .containerBackground)) ) .frame(width: min(networkSymbolLength, maxNetworkSymbolLength ?? networkSymbolLength), height: min(networkSymbolLength, maxNetworkSymbolLength ?? networkSymbolLength)) } @@ -150,7 +150,7 @@ struct NFTIconView: View { .overlay( Circle() .stroke(lineWidth: 2) - .foregroundColor(.white) + .foregroundColor(Color(braveSystemName: .containerBackground)) ) .frame( width: min(tokenLogoLength, maxTokenLogoLength ?? tokenLogoLength), diff --git a/Sources/BraveWallet/Crypto/CryptoPagesView.swift b/Sources/BraveWallet/Crypto/CryptoPagesView.swift index 771aed780e4..50fce28d328 100644 --- a/Sources/BraveWallet/Crypto/CryptoPagesView.swift +++ b/Sources/BraveWallet/Crypto/CryptoPagesView.swift @@ -132,7 +132,6 @@ struct CryptoPagesView: View { CryptoPagesViewController( keyringStore: keyringStore, cryptoStore: cryptoStore, - buySendSwapDestination: context.environment.buySendSwapDestination, isShowingPendingRequest: isShowingPendingRequest ) } @@ -145,21 +144,17 @@ struct CryptoPagesView: View { private class CryptoPagesViewController: TabbedPageViewController { private let keyringStore: KeyringStore private let cryptoStore: CryptoStore - private let swapButton = SwapButton() let pendingRequestsButton = ConfirmationsButton() - @Binding private var buySendSwapDestination: BuySendSwapDestination? @Binding private var isShowingPendingRequest: Bool init( keyringStore: KeyringStore, cryptoStore: CryptoStore, - buySendSwapDestination: Binding, isShowingPendingRequest: Binding ) { self.keyringStore = keyringStore self.cryptoStore = cryptoStore - self._buySendSwapDestination = buySendSwapDestination self._isShowingPendingRequest = isShowingPendingRequest super.init(nibName: nil, bundle: nil) } @@ -187,16 +182,6 @@ private class CryptoPagesViewController: TabbedPageViewController { ).then { $0.title = Strings.Wallet.portfolioPageTitle }, - UIHostingController( - rootView: NFTView( - cryptoStore: cryptoStore, - keyringStore: keyringStore, - networkStore: cryptoStore.networkStore, - nftStore: cryptoStore.nftStore - ) - ).then { - $0.title = Strings.Wallet.nftPageTitle - }, UIHostingController( rootView: TransactionsActivityView( store: cryptoStore.transactionsActivityStore, @@ -223,23 +208,10 @@ private class CryptoPagesViewController: TabbedPageViewController { }, ] - view.addSubview(swapButton) - swapButton.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.bottom.equalTo(view.safeAreaLayoutGuide).priority(.high) - $0.bottom.lessThanOrEqualTo(view).inset(8) - } - - pages.forEach { - $0.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: swapButton.intrinsicContentSize.height + 8, right: 0) - } - - swapButton.addTarget(self, action: #selector(tappedSwapButton), for: .touchUpInside) - view.addSubview(pendingRequestsButton) pendingRequestsButton.snp.makeConstraints { $0.trailing.equalToSuperview().inset(16) - $0.centerY.equalTo(swapButton) + $0.bottom.equalTo(view.safeAreaLayoutGuide).priority(.high) $0.bottom.lessThanOrEqualTo(view).inset(8) } pendingRequestsButton.addTarget(self, action: #selector(tappedPendingRequestsButton), for: .touchUpInside) @@ -248,25 +220,6 @@ private class CryptoPagesViewController: TabbedPageViewController { @objc private func tappedPendingRequestsButton() { isShowingPendingRequest = true } - - @objc private func tappedSwapButton() { - let controller = FixedHeightHostingPanModalController( - rootView: BuySendSwapView( - networkStore: cryptoStore.networkStore, - action: { [weak self] destination in - self?.dismiss( - animated: true, - completion: { - self?.buySendSwapDestination = destination - }) - }) - ) - presentPanModal( - controller, - sourceView: swapButton, - sourceRect: swapButton.bounds - ) - } } private class ConfirmationsButton: SpringButton { diff --git a/Sources/BraveWallet/Crypto/NFT/NFTView.swift b/Sources/BraveWallet/Crypto/NFT/NFTView.swift index 6fb8c731319..a0c989e3fd2 100644 --- a/Sources/BraveWallet/Crypto/NFT/NFTView.swift +++ b/Sources/BraveWallet/Crypto/NFT/NFTView.swift @@ -104,14 +104,9 @@ struct NFTView: View { } private var filtersButton: some View { - Button(action: { - self.isPresentingFiltersDisplaySettings = true - }) { - Image(braveSystemName: "leo.tune") - .font(.footnote.weight(.medium)) - .foregroundColor(Color(.braveBlurpleTint)) - .clipShape(Rectangle()) - } + AssetButton(braveSystemName: "leo.filter.settings", action: { + isPresentingFiltersDisplaySettings = true + }) .sheet(isPresented: $isPresentingFiltersDisplaySettings) { FiltersDisplaySettingsView( filters: nftStore.filters, @@ -138,9 +133,8 @@ struct NFTView: View { private var nftHeaderView: some View { HStack { Text(Strings.Wallet.assetsTitle) - .font(.footnote) - .foregroundColor(Color(.secondaryBraveLabel)) - .padding(.leading, 16) + .font(.title3.weight(.semibold)) + .foregroundColor(Color(braveSystemName: .textPrimary)) if nftStore.isLoadingDiscoverAssets && isNFTDiscoveryEnabled { ProgressView() .padding(.leading, 5) @@ -161,16 +155,13 @@ struct NFTView: View { .padding(.trailing, 10) addCustomAssetButton } - .textCase(nil) - .padding(.horizontal, 10) + .padding(.horizontal) .frame(maxWidth: .infinity, alignment: .leading) } private var addCustomAssetButton: some View { - Button(action: { + AssetButton(braveSystemName: "leo.plus.add") { isShowingAddCustomNFT = true - }) { - Image(systemName: "plus") } } @@ -229,6 +220,7 @@ struct NFTView: View { } } } + .padding(.horizontal) VStack(spacing: 16) { Divider() editUserAssetsButton @@ -238,16 +230,11 @@ struct NFTView: View { } var body: some View { - ScrollView { - VStack { - nftHeaderView - .padding(.horizontal, 8) - nftGridsView - .padding(.horizontal, 24) - } - .padding(.vertical, 24) + VStack(spacing: 16) { + nftHeaderView + + nftGridsView } - .background(Color(UIColor.braveGroupedBackground)) .background( NavigationLink( isActive: Binding( diff --git a/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift b/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift new file mode 100644 index 00000000000..4fa0522fca1 --- /dev/null +++ b/Sources/BraveWallet/Crypto/Portfolio/AssetButton.swift @@ -0,0 +1,29 @@ +/* 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/PortfolioAssetView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetView.swift index 06952337f17..23040d3e8b1 100644 --- a/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetView.swift +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetView.swift @@ -22,10 +22,12 @@ struct PortfolioAssetView: View { accessoryContent: { VStack(alignment: .trailing) { Text(amount.isEmpty ? "0.0" : amount) + .fontWeight(.semibold) Text(verbatim: "\(quantity) \(symbol)") } .font(.footnote) .foregroundColor(Color(.braveLabel)) + .multilineTextAlignment(.trailing) } ) .accessibilityLabel("\(title), \(quantity) \(symbol), \(amount)") @@ -109,6 +111,7 @@ struct AssetView: View { .font(.caption) .foregroundColor(Color(.braveLabel)) } + .multilineTextAlignment(.leading) Spacer() accessoryContent() } diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift new file mode 100644 index 00000000000..b423c9529e3 --- /dev/null +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioAssetsView.swift @@ -0,0 +1,235 @@ +/* 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 +import BraveCore + +struct PortfolioAssetsView: View { + + var cryptoStore: CryptoStore + @ObservedObject var keyringStore: KeyringStore + @ObservedObject var networkStore: NetworkStore + @ObservedObject var portfolioStore: PortfolioStore + + @State private var isPresentingEditUserAssets: Bool = false + @State private var isPresentingFiltersDisplaySettings: Bool = false + @State private var selectedToken: BraveWallet.BlockchainToken? + @State private var groupToggleState: [AssetGroupViewModel.ID: Bool] = [:] + + var body: some View { + LazyVStack(spacing: 16) { + assetSectionsHeader + + if portfolioStore.isShowingAssetsLoadingState { + SkeletonLoadingAssetView() + } else if portfolioStore.isShowingAssetsEmptyState { + emptyAssetsState + } else { + ForEach(portfolioStore.assetGroups) { group in + if group.groupType == .none { + ungroupedAssets(group) + } else { + groupedAssetsSection(for: group) + } + } + } + } + .background( + NavigationLink( + isActive: Binding( + get: { selectedToken != nil }, + set: { if !$0 { selectedToken = nil } } + ), + destination: { + if let token = selectedToken { + AssetDetailView( + assetDetailStore: cryptoStore.assetDetailStore(for: .blockchainToken(token)), + keyringStore: keyringStore, + networkStore: cryptoStore.networkStore + ) + .onDisappear { + cryptoStore.closeAssetDetailStore(for: .blockchainToken(token)) + } + } + }, + label: { + EmptyView() + }) + ) + } + + /// Header for the assets section(s) containing the title, edit user assets and filter buttons + private var assetSectionsHeader: some View { + HStack { + Text(Strings.Wallet.assetsTitle) + .foregroundColor(Color(braveSystemName: .textPrimary)) + .font(.title3.weight(.semibold)) + if portfolioStore.isLoadingDiscoverAssets { + ProgressView() + .padding(.leading, 5) + } + Spacer() + editUserAssetsButton + .padding(.trailing, 10) + filtersButton + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + } + + private var editUserAssetsButton: some View { + AssetButton(braveSystemName: "leo.list.settings", action: { + isPresentingEditUserAssets = true + }) + .sheet(isPresented: $isPresentingEditUserAssets) { + EditUserAssetsView( + networkStore: networkStore, + keyringStore: keyringStore, + userAssetsStore: portfolioStore.userAssetsStore + ) { + cryptoStore.updateAssets() + } + } + } + + private var filtersButton: some View { + AssetButton(braveSystemName: "leo.filter.settings", action: { + isPresentingFiltersDisplaySettings = true + }) + .sheet(isPresented: $isPresentingFiltersDisplaySettings) { + FiltersDisplaySettingsView( + filters: portfolioStore.filters, + isNFTFilters: false, + networkStore: networkStore, + save: { filters in + portfolioStore.saveFilters(filters) + } + ) + .osAvailabilityModifiers({ view in + if #available(iOS 16, *) { + view + .presentationDetents([ + .fraction(0.7), + .large + ]) + } else { + view + } + }) + } + } + + private var emptyAssetsState: some View { + VStack(spacing: 10) { + Image("portfolio-empty", bundle: .module) + .aspectRatio(contentMode: .fit) + Text(Strings.Wallet.portfolioEmptyStateTitle) + .font(.headline) + .foregroundColor(Color(WalletV2Design.textPrimary)) + Text(Strings.Wallet.portfolioEmptyStateDescription) + .font(.footnote) + .foregroundColor(Color(WalletV2Design.textSecondary)) + } + .multilineTextAlignment(.center) + .padding(.vertical) + } + + /// Builds the list of assets without any grouping or expandable / collapse behaviour. + @ViewBuilder private func ungroupedAssets(_ group: AssetGroupViewModel) -> some View { + ForEach(group.assets) { asset in + Button(action: { + selectedToken = asset.token + }) { + PortfolioAssetView( + image: AssetIconView( + token: asset.token, + network: asset.network, + shouldShowNetworkIcon: true + ), + title: asset.token.name, + symbol: asset.token.symbol, + networkName: asset.network.chainName, + amount: asset.fiatAmount(currencyFormatter: portfolioStore.currencyFormatter), + quantity: asset.quantity + ) + } + } + .padding(.horizontal) + } + + /// Builds the expandable/collapseable (expanded by default) section content for a given group. + @ViewBuilder private func groupedAssetsSection(for group: AssetGroupViewModel) -> some View { + WalletDisclosureGroup( + isExpanded: Binding( + get: { groupToggleState[group.id, default: true] }, + set: { isExpanded in + groupToggleState[group.id] = isExpanded + } + ), + content: { + ForEach(group.assets) { asset in + Button(action: { + selectedToken = asset.token + }) { + PortfolioAssetView( + image: AssetIconView( + token: asset.token, + network: asset.network, + shouldShowNetworkIcon: true + ), + title: asset.token.name, + symbol: asset.token.symbol, + networkName: asset.network.chainName, + amount: asset.fiatAmount(currencyFormatter: portfolioStore.currencyFormatter), + quantity: asset.quantity + ) + } + } + }, + label: { + if case let .account(account) = group.groupType { + AddressView(address: account.address) { + groupHeader(for: group) + } + } else { + groupHeader(for: group) + } + } + ) + } + + /// Builds the in-section header for an AssetGroupViewModel that is shown in expanded and non-expanded state. Not used for ungrouped assets. + private func groupHeader(for group: AssetGroupViewModel) -> some View { + VStack(spacing: 0) { + HStack { + if case let .network(networkInfo) = group.groupType { + NetworkIcon(network: networkInfo, length: 32) + } else if case let .account(accountInfo) = group.groupType { + Blockie(address: accountInfo.address, shape: .rectangle) + .frame(width: 32, height: 32) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + VStack(alignment: .leading) { + Text(group.title) + .font(.callout.weight(.semibold)) + .foregroundColor(Color(WalletV2Design.textPrimary)) + if let description = group.description { + Text(description) + .font(.footnote) + .foregroundColor(Color(WalletV2Design.textSecondary)) + } + } + .multilineTextAlignment(.leading) + Spacer() + Text(portfolioStore.currencyFormatter.string(from: NSNumber(value: group.totalFiatValue)) ?? "") + .font(.callout.weight(.semibold)) + .foregroundColor(Color(WalletV2Design.textPrimary)) + .multilineTextAlignment(.trailing) + } + .padding(.vertical, 4) + } + } +} diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift new file mode 100644 index 00000000000..d08ff8de8a7 --- /dev/null +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioHeaderView.swift @@ -0,0 +1,220 @@ +/* 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 +import BraveCore + +struct PortfolioHeaderView: View { + + @ObservedObject var keyringStore: KeyringStore + @Binding var buySendSwapDestination: BuySendSwapDestination? + @Binding var selectedDateRange: BraveWallet.AssetPriceTimeframe + var balance: String + var balanceDifference: BalanceDifference? + var historicalBalances: [BalanceTimePrice] + var isLoading: Bool + + @State private var isPresentingBackup = false + @State private var dismissedBackupBannerThisSession = false + @State private var selectedBalance: BalanceTimePrice? + + private var isShowingBackupBanner: Bool { + !keyringStore.defaultKeyring.isBackedUp && !dismissedBackupBannerThisSession + } + + private var emptyBalanceData: [BalanceTimePrice] { + // About 300 points added so it doesn't animate funny + (0..<300).map { _ in .init(date: Date(), price: 0.0, formattedPrice: "") } + } + + var body: some View { + VStack(spacing: 0) { + if isShowingBackupBanner { + backupBanner + + Spacer().frame(height: 10) + } + + balanceAndPriceChanges + + Spacer().frame(height: 24) + + buySendSwapButtons + + Spacer().frame(height: 24) + + lineChart + } + .padding() + .frame(maxWidth: .infinity) + .background(Color(braveSystemName: .pageBackground)) + } + + private var backupBanner: some View { + BackupNotifyView( + action: { + isPresentingBackup = true + }, + onDismiss: { + // Animating this doesn't seem to work in SwiftUI.. will keep an eye out for iOS 15 + dismissedBackupBannerThisSession = true + } + ) + .buttonStyle(PlainButtonStyle()) + .padding([.top, .leading, .trailing], 12) + .sheet(isPresented: $isPresentingBackup) { + NavigationView { + BackupWalletView( + password: nil, + keyringStore: keyringStore + ) + } + .environment(\.modalPresentationMode, $isPresentingBackup) + .accentColor(Color(.braveBlurpleTint)) + } + } + + private var balanceAndPriceChanges: some View { + VStack(spacing: 12) { + Text(balance) + .frame(maxWidth: .infinity) + .opacity(selectedBalance == nil ? 1 : 0) + .overlay( + Group { + if let dataPoint = selectedBalance { + Text(dataPoint.formattedPrice) + } + } + ) + .font(.largeTitle.weight(.medium)) + .multilineTextAlignment(.center) + + if let balanceDifference { + HStack { + Text(balanceDifference.priceDifference) + .font(.footnote) + .foregroundColor(Color(braveSystemName: balanceDifference.isBalanceUp ? .systemfeedbackSuccessText : .systemfeedbackErrorText)) + Text(balanceDifference.percentageChange) + .font(.footnote) + .padding(4) + .foregroundColor(Color(braveSystemName: balanceDifference.isBalanceUp ? .green50 : .red50)) + .background(Color(braveSystemName: balanceDifference.isBalanceUp ? .green20 : .red20).cornerRadius(4)) + } + } + } + } + + private var buySendSwapButtons: some View { + HStack(spacing: 24) { + PortfolioHeaderButton(style: .buy) { + buySendSwapDestination = BuySendSwapDestination(kind: .buy) + } + PortfolioHeaderButton(style: .send) { + buySendSwapDestination = BuySendSwapDestination(kind: .send) + } + PortfolioHeaderButton(style: .swap) { + buySendSwapDestination = BuySendSwapDestination(kind: .swap) + } + } + .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 + let chartData = historicalBalances.isEmpty ? emptyBalanceData : historicalBalances + LineChartView(data: chartData, numberOfColumns: chartData.count, selectedDataPoint: $selectedBalance) { + LinearGradient(braveGradient: .lightGradient02) + .shimmer(isLoading) + } + .chartAccessibility( + title: Strings.Wallet.portfolioPageTitle, + dataPoints: chartData + ) + .disabled(historicalBalances.isEmpty) + .frame(height: 148) + .padding(.horizontal, -12) + .animation(.default, value: historicalBalances) + } + } +} + +private struct PortfolioHeaderButton: View { + + enum Style: String, Equatable { + case buy, send, swap, more + + var label: String { + rawValue.capitalizeFirstLetter + } + + var iconName: String { + switch self { + case .buy: return "leo.coins.alt1" + case .send: return "leo.send" + case .swap: return "leo.currency.exchange" + case .more: return "leo.more.horizontal" + } + } + } + + let style: Style + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack { + Circle() + .fill(Color(braveSystemName: .buttonBackground)) + .frame(width: 48, height: 48) + .overlay( + Image(braveSystemName: style.iconName) + .foregroundColor(.white) + .font(.title3.weight(.semibold)) + .dynamicTypeSize(...DynamicTypeSize.xLarge) + ) + Text(style.label) + .font(.footnote.weight(.semibold)) + .foregroundColor(Color(braveSystemName: .textPrimary)) + } + } + } +} diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioSegmentedControl.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioSegmentedControl.swift new file mode 100644 index 00000000000..a015e56b387 --- /dev/null +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioSegmentedControl.swift @@ -0,0 +1,195 @@ +/* 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 UIKit +import SwiftUI +import BraveCore +import Strings +import DesignSystem +import BraveUI +import Shared + +struct PortfolioSegmentedControl: View { + + enum SelectedContent: Int, Equatable, CaseIterable, Identifiable { + case assets + case nfts + + var displayText: String { + switch self { + case .assets: return Strings.Wallet.assetsTitle + case .nfts: return Strings.Wallet.nftsTitle + } + } + + var id: Int { rawValue } + } + + @Binding var selected: SelectedContent + @State private var viewSize: CGSize = .zero + @State private var location: CGPoint = .zero + @GestureState private var isDragGestureActive: Bool = false + + var body: some View { + GeometryReader { geometryProxy in + Capsule() + .fill(Color(braveSystemName: .containerHighlight)) + .osAvailabilityModifiers { + if #unavailable(iOS 16) { + $0.overlay { + // TapGesture does not give a location, + // SpatialTapGesture is iOS 16+. + HStack { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + select(.assets) + } + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + select(.nfts) + } + } + } + } else { + $0 + } + } + .overlay { + Capsule() + .fill(Color(braveSystemName: .containerBackground)) + .padding(4) + .frame(width: geometryProxy.size.width / 2) + .position(location) + } + .overlay { + HStack { + Spacer() + Text(SelectedContent.assets.displayText) + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(braveSystemName: selected == .assets ? .textPrimary : .textSecondary)) + .allowsHitTesting(false) + Spacer() + Spacer() + Text(SelectedContent.nfts.displayText) + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(braveSystemName: selected == .nfts ? .textPrimary : .textSecondary)) + .allowsHitTesting(false) + Spacer() + } + } + .readSize { size in + if location == .zero { + let newX = selected == .assets ? geometryProxy.size.width / 4 : geometryProxy.size.width / 4 * 3 + location = CGPoint( + x: newX, + y: geometryProxy.size.height / 2 + ) + } + viewSize = size + } + } + .frame(height: 40) + .gesture(dragGesture) + .onChange(of: isDragGestureActive) { isDragGestureActive in + if !isDragGestureActive { // cancellation of gesture, ex while scrolling + var newX = location.x + if newX < viewSize.width / 2 { + select(.assets) + } else { + select(.nfts) + } + } + } + .osAvailabilityModifiers { + if #available(iOS 16, *) { + $0.simultaneousGesture(tapGesture) + } else { + $0 + } + } + .accessibilityRepresentation { + Picker(selection: $selected) { + ForEach(SelectedContent.allCases) { content in + Text(content.displayText).tag(content) + } + } label: { + EmptyView() + } + .pickerStyle(.segmented) + } + } + + private var dragGesture: some Gesture { + DragGesture() + .updating($isDragGestureActive) { value, state, transaction in + state = true + } + .onChanged { value in + var newX = value.location.x + if newX < viewSize.width / 4 { + newX = viewSize.width / 4 + } else if newX > (viewSize.width / 4 * 3) { + newX = (viewSize.width / 4 * 3) + } + location = CGPoint( + x: newX, + y: location.y + ) + } + .onEnded { value in + if value.predictedEndLocation.x <= viewSize.width / 2 { + select(.assets) + } else { + select(.nfts) + } + } + } + + @available(iOS 16, *) + private var tapGesture: some Gesture { + SpatialTapGesture() + .onEnded { value in + if value.location.x < viewSize.width / 2 { + select(.assets) + } else { + select(.nfts) + } + } + } + + private func select(_ selectedContent: SelectedContent) { + selected = selectedContent + withAnimation(.spring()) { + var newX = viewSize.width / 4 + if selectedContent == .nfts { + newX *= 3 + } + location = CGPoint( + x: newX, + y: viewSize.height / 2 + ) + } + } +} + +// https://www.fivestars.blog/articles/swiftui-share-layout-information/ +struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} + +extension View { + func readSize(onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: onChange) + } +} diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift index a52676e8a0c..1e3470a64eb 100644 --- a/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift @@ -15,404 +15,76 @@ import Shared import Preferences struct PortfolioView: View { + var cryptoStore: CryptoStore @ObservedObject var keyringStore: KeyringStore @ObservedObject var networkStore: NetworkStore @ObservedObject var portfolioStore: PortfolioStore - - @State private var dismissedBackupBannerThisSession: Bool = false - @State private var isPresentingBackup: Bool = false - @State private var isPresentingEditUserAssets: Bool = false - @State private var isPresentingFiltersDisplaySettings: Bool = false - @Environment(\.sizeCategory) private var sizeCategory @Environment(\.buySendSwapDestination) private var buySendSwapDestination: Binding - /// Reference to the collection view used to back the `List` on iOS 16+ - @State private var collectionViewRef: WeakRef? - - private var isShowingBackupBanner: Bool { - !keyringStore.defaultKeyring.isBackedUp && !dismissedBackupBannerThisSession - } - - private var listHeader: some View { - VStack(spacing: 0) { - if isShowingBackupBanner { - BackupNotifyView( - action: { - isPresentingBackup = true - }, - onDismiss: { - // Animating this doesn't seem to work in SwiftUI.. will keep an eye out for iOS 15 - dismissedBackupBannerThisSession = true - } - ) - .buttonStyle(PlainButtonStyle()) - .padding([.top, .leading, .trailing], 12) - .sheet(isPresented: $isPresentingBackup) { - NavigationView { - BackupWalletView( - password: nil, - keyringStore: keyringStore - ) - } - .environment(\.modalPresentationMode, $isPresentingBackup) - .accentColor(Color(.braveBlurpleTint)) - } - } - BalanceHeaderView( - balance: portfolioStore.balance, - historicalBalances: portfolioStore.historicalBalances, - isLoading: portfolioStore.isLoadingBalances, - keyringStore: keyringStore, - networkStore: networkStore, - selectedDateRange: $portfolioStore.timeframe - ) - } - } - - @State private var tableInset: CGFloat = -16.0 - - @State private var selectedToken: BraveWallet.BlockchainToken? - private var editUserAssetsButton: some View { - Button(action: { isPresentingEditUserAssets = true }) { - Image(braveSystemName: "leo.list.settings") - .font(.footnote.weight(.medium)) - .foregroundColor(Color(.braveBlurpleTint)) - .clipShape(Rectangle()) - } - .sheet(isPresented: $isPresentingEditUserAssets) { - EditUserAssetsView( - networkStore: networkStore, - keyringStore: keyringStore, - userAssetsStore: portfolioStore.userAssetsStore - ) { - cryptoStore.updateAssets() - } - } - } + @State private var selectedContent: PortfolioSegmentedControl.SelectedContent = .assets - private var filtersButton: some View { - Button(action: { - self.isPresentingFiltersDisplaySettings = true - }) { - Image(braveSystemName: "leo.tune") - .font(.footnote.weight(.medium)) - .foregroundColor(Color(.braveBlurpleTint)) - .clipShape(Rectangle()) - } - .sheet(isPresented: $isPresentingFiltersDisplaySettings) { - FiltersDisplaySettingsView( - filters: portfolioStore.filters, - isNFTFilters: false, - networkStore: networkStore, - save: { filters in - portfolioStore.saveFilters(filters) - } - ) - .osAvailabilityModifiers({ view in - if #available(iOS 16, *) { - view - .presentationDetents([ - .fraction(0.7), - .large - ]) - } else { - view - } - }) - } - } - var body: some View { - List { - Section( - header: - listHeader - .padding(.horizontal, tableInset) // inset grouped layout margins workaround - .resetListHeaderStyle() - ) { } - - assetSections - } - .background( - NavigationLink( - isActive: Binding( - get: { selectedToken != nil }, - set: { if !$0 { selectedToken = nil } } - ), - destination: { - if let token = selectedToken { - AssetDetailView( - assetDetailStore: cryptoStore.assetDetailStore(for: .blockchainToken(token)), - keyringStore: keyringStore, - networkStore: cryptoStore.networkStore - ) - .onDisappear { - cryptoStore.closeAssetDetailStore(for: .blockchainToken(token)) - } - } - }, - label: { - EmptyView() - }) - ) - .animation(.default, value: portfolioStore.assetGroups) - .listStyle(InsetGroupedListStyle()) - .listBackgroundColor(Color(UIColor.braveGroupedBackground)) - .introspectTableView { tableView in - withAnimation(nil) { - tableInset = -tableView.layoutMargins.left - } - } - .onChange(of: sizeCategory) { _ in - // Fix broken header when text size changes on iOS 16+ - self.collectionViewRef?.value?.collectionViewLayout.invalidateLayout() - } - .introspect( - selector: TargetViewSelector.ancestorOrSiblingContaining - ) { (collectionView: UICollectionView) in - self.collectionViewRef = .init(collectionView) - } - } - - /// Header for the assets section(s) containing the title, edit user assets and filter buttons - private var assetSectionsHeader: some View { - HStack { - Text(Strings.Wallet.assetsTitle) - if portfolioStore.isLoadingDiscoverAssets { - ProgressView() - .padding(.leading, 5) - } - Spacer() - editUserAssetsButton - .padding(.trailing, 10) - filtersButton - } - .textCase(nil) - .padding(.horizontal, -8) - .frame(maxWidth: .infinity, alignment: .leading) - } - - private var emptyAssetsState: some View { - VStack(spacing: 10) { - Image("portfolio-empty", bundle: .module) - .aspectRatio(contentMode: .fit) - Text(Strings.Wallet.portfolioEmptyStateTitle) - .font(.headline) - .foregroundColor(Color(WalletV2Design.textPrimary)) - Text(Strings.Wallet.portfolioEmptyStateDescription) - .font(.footnote) - .foregroundColor(Color(WalletV2Design.textSecondary)) - } - .multilineTextAlignment(.center) - .padding(.vertical) - } - - /// Grouped / Ungrouped asset `Section`s for the List - @ViewBuilder private var assetSections: some View { - if portfolioStore.isShowingAssetsLoadingState || portfolioStore.isShowingAssetsEmptyState { - Section(content: { - Group { - if portfolioStore.isShowingAssetsLoadingState { - SkeletonLoadingAssetView() - } else { // isShowingAssetsEmptyState - emptyAssetsState - } - } - .listRowBackground(Color(.secondaryBraveGroupedBackground)) - }, header: { - assetSectionsHeader - }) - } else { - ForEach(portfolioStore.assetGroups) { group in - Section( - content: { - if group.groupType == .none { - ungroupedAssets(group) - .listRowBackground(Color(.secondaryBraveGroupedBackground)) - } else { - groupedAssetsSection(for: group) - .listRowBackground(Color(.secondaryBraveGroupedBackground)) - } - }, - header: { - if group == portfolioStore.assetGroups.first { - assetSectionsHeader - } - } + ScrollView { + VStack(spacing: 0) { + PortfolioHeaderView( + keyringStore: keyringStore, + buySendSwapDestination: buySendSwapDestination, + selectedDateRange: $portfolioStore.timeframe, + balance: portfolioStore.balance, + balanceDifference: portfolioStore.balanceDifference, + historicalBalances: portfolioStore.historicalBalances, + isLoading: portfolioStore.isLoadingBalances ) + contentDrawer } } - } - - /// Builds the list of assets without any grouping or expandable / collapse behaviour. - @ViewBuilder private func ungroupedAssets(_ group: AssetGroupViewModel) -> some View { - ForEach(group.assets) { asset in - Button(action: { - selectedToken = asset.token - }) { - PortfolioAssetView( - image: AssetIconView( - token: asset.token, - network: asset.network, - shouldShowNetworkIcon: true - ), - title: asset.token.name, - symbol: asset.token.symbol, - networkName: asset.network.chainName, - amount: asset.fiatAmount(currencyFormatter: portfolioStore.currencyFormatter), - quantity: asset.quantity - ) - } - } - } - - @State var groupToggleState: [AssetGroupViewModel.ID: Bool] = [:] - - /// Builds the expandable/collapseable (expanded by default) section content for a given group. - @ViewBuilder private func groupedAssetsSection(for group: AssetGroupViewModel) -> some View { - WalletDisclosureGroup( - isExpanded: Binding( - get: { groupToggleState[group.id, default: true] }, - set: { isExpanded in - groupToggleState[group.id] = isExpanded - } - ), - content: { - ForEach(group.assets) { asset in - Button(action: { - selectedToken = asset.token - }) { - PortfolioAssetView( - image: AssetIconView( - token: asset.token, - network: asset.network, - shouldShowNetworkIcon: true - ), - title: asset.token.name, - symbol: asset.token.symbol, - networkName: asset.network.chainName, - amount: asset.fiatAmount(currencyFormatter: portfolioStore.currencyFormatter), - quantity: asset.quantity - ) - } - } - }, - label: { - if case let .account(account) = group.groupType { - AddressView(address: account.address) { - groupHeader(for: group) - } - } else { - groupHeader(for: group) - } - } + .background( + VStack(spacing: 0) { + Color(braveSystemName: .pageBackground) // top scroll rubberband area + Color(braveSystemName: .containerBackground) // bottom drawer scroll rubberband area + }.edgesIgnoringSafeArea(.bottom) ) } - /// Builds the in-section header for an AssetGroupViewModel that is shown in expanded and non-expanded state. Not used for ungrouped assets. - private func groupHeader(for group: AssetGroupViewModel) -> some View { - VStack(spacing: 0) { - HStack { - if case let .network(networkInfo) = group.groupType { - NetworkIcon(network: networkInfo, length: 32) - } else if case let .account(accountInfo) = group.groupType { - Blockie(address: accountInfo.address, shape: .rectangle) - .frame(width: 32, height: 32) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } - VStack(alignment: .leading) { - Text(group.title) - .font(.callout.weight(.semibold)) - .foregroundColor(Color(WalletV2Design.textPrimary)) - if let description = group.description { - Text(description) - .font(.footnote) - .foregroundColor(Color(WalletV2Design.textSecondary)) - } - } - .multilineTextAlignment(.leading) - Spacer() - Text(portfolioStore.currencyFormatter.string(from: NSNumber(value: group.totalFiatValue)) ?? "") - .font(.callout.weight(.semibold)) - .foregroundColor(Color(WalletV2Design.textPrimary)) - .multilineTextAlignment(.trailing) - } - .padding(.vertical, 4) - } - } -} - -struct BalanceHeaderView: View { - var balance: String - var historicalBalances: [BalanceTimePrice] - var isLoading: Bool - var keyringStore: KeyringStore - @ObservedObject var networkStore: NetworkStore - @Binding var selectedDateRange: BraveWallet.AssetPriceTimeframe - - @Environment(\.sizeCategory) private var sizeCategory - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - - @State private var selectedBalance: BalanceTimePrice? - - private var balanceOrDataPointView: some View { - HStack { - Text(verbatim: balance) - .font(.largeTitle.bold()) - .frame(maxWidth: .infinity, alignment: .leading) - .opacity(selectedBalance == nil ? 1 : 0) - .overlay( - Group { - if let dataPoint = selectedBalance { - Text(dataPoint.formattedPrice) - .font(.largeTitle.bold()) - } - }, - alignment: .leading - ) - if horizontalSizeClass == .regular { - Spacer() - DateRangeView(selectedRange: $selectedDateRange) - .padding(6) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder(Color(.secondaryButtonTint)) + private var contentDrawer: some View { + LazyVStack { + PortfolioSegmentedControl(selected: $selectedContent) + .padding(.horizontal) + .padding(.bottom, 6) + Group { + if selectedContent == .assets { + PortfolioAssetsView( + cryptoStore: cryptoStore, + keyringStore: keyringStore, + networkStore: networkStore, + portfolioStore: portfolioStore ) + .padding(.horizontal, 8) + } else { + NFTView( + cryptoStore: cryptoStore, + keyringStore: keyringStore, + networkStore: cryptoStore.networkStore, + nftStore: cryptoStore.nftStore + ) + .padding(.horizontal, 8) + } } } - .foregroundColor(.primary) - .padding(.top, 12) - } - - private var emptyBalanceData: [BalanceTimePrice] { - // About 300 points added so it doesn't animate funny - (0..<300).map { _ in .init(date: Date(), price: 0.0, formattedPrice: "") } - } - - var body: some View { - let chartData = historicalBalances.isEmpty ? emptyBalanceData : historicalBalances - VStack(alignment: .leading, spacing: 4) { - balanceOrDataPointView - LineChartView(data: chartData, numberOfColumns: chartData.count, selectedDataPoint: $selectedBalance) { - LinearGradient(braveGradient: .lightGradient02) - .shimmer(isLoading) - } - .chartAccessibility( - title: Strings.Wallet.portfolioPageTitle, - dataPoints: chartData - ) - .disabled(historicalBalances.isEmpty) - .frame(height: 148) - .padding(.horizontal, -12) - .animation(.default, value: historicalBalances) - if horizontalSizeClass == .compact { - DateRangeView(selectedRange: $selectedDateRange) + .padding(.vertical) + .background( + ZStack { + Color(braveSystemName: .pageBackground) // bg behind rounded corners + .zIndex(0) + Color(braveSystemName: .containerBackground) + .roundedCorner(16, corners: [.topLeft, .topRight]) + .shadow(color: .black.opacity(0.04), radius: 8, x: 0, y: -8) + .zIndex(1) } - } - .padding(12) + ) } } @@ -432,3 +104,26 @@ struct PortfolioViewController_Previews: PreviewProvider { } } #endif + +private struct RoundedRect: Shape { + var radius: CGFloat + var corners: UIRectCorner + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize( + width: radius, + height: radius + ) + ) + return Path(path.cgPath) + } +} + +extension View { + func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedRect(radius: radius, corners: corners) ) + } +} diff --git a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift index 61c19499bac..f01e687c903 100644 --- a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift @@ -186,10 +186,18 @@ struct BalanceTimePrice: DataPoint, Equatable { } } +struct BalanceDifference: Equatable { + let priceDifference: String + let percentageChange: String + let isBalanceUp: Bool +} + /// A store containing data around the users assets public class PortfolioStore: ObservableObject, WalletObserverStore { /// The dollar amount of your portfolio @Published private(set) var balance: String = "$0.00" + /// Balance difference used to display difference between first point on the graph and current balance. + @Published private(set) var balanceDifference: BalanceDifference? /// The users visible fungible token groups. @Published private(set) var assetGroups: [AssetGroupViewModel] = [] /// The timeframe of the portfolio @@ -502,6 +510,26 @@ public class PortfolioStore: ObservableObject, WalletObserverStore { formattedPrice: currencyFormatter.string(from: NSNumber(value: value)) ?? "0.00" ) } + + if let oldestHistoricalValue = historicalBalances.first { + let priceDifference = currentBalance - oldestHistoricalValue.price + let percentageChange = priceDifference / oldestHistoricalValue.price * 100 + let isBalanceUp = priceDifference > 0 + balanceDifference = .init( + priceDifference: String( + format: "%@%@", + isBalanceUp ? "+" : "", // include plus if balance increased + currencyFormatter.string(from: NSNumber(value: priceDifference)) ?? "\(priceDifference)"), + percentageChange: String( + format: "%@%.2f%%", + isBalanceUp ? "+" : "", // include plus if balance increased + percentageChange), + isBalanceUp: isBalanceUp + ) + } else { // don't display difference / percentage change + balanceDifference = nil + } + isLoadingBalances = false } } diff --git a/Sources/BraveWallet/Crypto/WalletDisclosureGroup.swift b/Sources/BraveWallet/Crypto/WalletDisclosureGroup.swift index 7bc06300622..c8f8d4ff559 100644 --- a/Sources/BraveWallet/Crypto/WalletDisclosureGroup.swift +++ b/Sources/BraveWallet/Crypto/WalletDisclosureGroup.swift @@ -10,22 +10,56 @@ struct WalletDisclosureGroup: View { @ViewBuilder var content: () -> Content @ViewBuilder var label: () -> Label - var body: some View { + private var header: some View { HStack { label() Spacer() Image(braveSystemName: "leo.carat.down") .rotationEffect(.degrees(isExpanded ? 180 : 0)) .foregroundColor(Color(.braveBlurpleTint)) + .animation(.default, value: isExpanded) + } + .padding(.horizontal) + // when expanded, padding is applied to entire `LazyVStack` + .padding(.vertical, isExpanded ? 0 : 6) + .osAvailabilityModifiers { + if !isExpanded { + $0.overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(Color(braveSystemName: .dividerSubtle), lineWidth: 1) + } + } else { + $0 + } } .contentShape(Rectangle()) .onTapGesture { - withAnimation { - isExpanded.toggle() + isExpanded.toggle() + } + } + + var body: some View { + LazyVStack { + header + if isExpanded { + Divider() + .padding(.top, 6) + .padding(.horizontal, 8) + content() + .padding(.horizontal) } } - if isExpanded { - content() + // when collapsed, padding is applied to `header` + .padding(.vertical, isExpanded ? 6 : 0) + .osAvailabilityModifiers { + if isExpanded { + $0.overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(Color(braveSystemName: .dividerSubtle), lineWidth: 1) + } + } else { + $0 + } } } } diff --git a/Sources/BraveWallet/SwapButton.swift b/Sources/BraveWallet/SwapButton.swift deleted file mode 100644 index 0cf54d2d463..00000000000 --- a/Sources/BraveWallet/SwapButton.swift +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright 2021 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 Foundation -import UIKit -import BraveUI -import DesignSystem -import Strings - -class SwapButton: SpringButton { - private let gradientView = BraveGradientView.alternateGradient02.then { - $0.isUserInteractionEnabled = false - $0.clipsToBounds = true - } - private let imageView = UIImageView(image: UIImage(named: "swap", in: .module, compatibleWith: nil)).then { - $0.isUserInteractionEnabled = false - } - - override init(frame: CGRect) { - super.init(frame: frame) - - imageView.contentMode = .scaleAspectFit - - addSubview(gradientView) - addSubview(imageView) - - gradientView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - imageView.snp.makeConstraints { - $0.center.equalToSuperview() - } - snp.makeConstraints { - $0.width.equalTo(snp.height) - } - - layer.shadowColor = UIColor.black.cgColor - layer.shadowOffset = .init(width: 0, height: 1) - layer.shadowRadius = 1 - layer.shadowOpacity = 0.3 - - accessibilityLabel = ListFormatter.localizedString( - byJoining: [Strings.Wallet.buy, Strings.Wallet.send, Strings.Wallet.swap] - ) - } - - override func layoutSubviews() { - super.layoutSubviews() - - gradientView.layer.cornerRadius = bounds.height / 2.0 - layer.shadowPath = UIBezierPath(ovalIn: bounds).cgPath - } - - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError() - } - - override var intrinsicContentSize: CGSize { - .init(width: 44, height: 44) - } -} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.coins.alt1.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.coins.alt1.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.coins.alt1.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.currency.exchange.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.currency.exchange.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.currency.exchange.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.filter.settings.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.filter.settings.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.filter.settings.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.send.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.send.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.send.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +}