mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
feat: redesign for CurrencyPicker and FiatPicker, introduce recents and currency groupings (#3248)
* Redesign for CurrencyPicker and FiatPicker, introduce recents and currency groupings * chore: cleanup * refactor: switch recents to previously completed trades, specify stables to be displayed, fix scrollable and adjust badge * refactor: switch recents to previously completed trades, specify stables to be displayed, fix scrollable, adjust badge, add fiat and multinetwork currency pickers to buy and sell flows * fresh set of changes and fixes for new currency picker flow * add chain label to items on search and smooth pop * add chain pill and label to xstocks * properly filter search results * shorten chain pill name for BSC tokens * Review fixes * cleanups * safe pop --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Omar Hatem
parent
4365462279
commit
8af85351e9
@@ -4,7 +4,6 @@ import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
@@ -13,6 +12,9 @@ import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/picker.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart';
|
||||
@@ -36,7 +38,6 @@ import 'package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart';
|
||||
import 'package:cake_wallet/routes.dart' show Routes;
|
||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
|
||||
@@ -51,7 +52,6 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
@@ -809,26 +809,41 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
|
||||
final output = widget.sendViewModel.outputs[_selectedOutput];
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
key: ValueKey('send_page_currency_picker_dialog_button_key'),
|
||||
selectedAtIndex: _fiatInputMode
|
||||
? widget.sendViewModel.fiatCurrencies.indexOf(widget.sendViewModel.fiatCurrency)
|
||||
: widget.sendViewModel.currencies.indexOf(widget.sendViewModel.selectedCryptoCurrency),
|
||||
items:
|
||||
_fiatInputMode ? widget.sendViewModel.fiatCurrencies : widget.sendViewModel.currencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency cur) async {
|
||||
late final selectedCurrency;
|
||||
if (_fiatInputMode) {
|
||||
selectedCurrency = widget.sendViewModel.setFiatCurrency(cur as FiatCurrency);
|
||||
} else {
|
||||
selectedCurrency =
|
||||
widget.sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency);
|
||||
}
|
||||
if (_fiatInputMode) {
|
||||
FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: widget.sendViewModel.fiatCurrency,
|
||||
onSelected: (cur) async {
|
||||
widget.sendViewModel.setFiatCurrency(cur);
|
||||
await output.calculateEstimatedFee();
|
||||
return selectedCurrency;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final isFiatDisabled = widget.sendViewModel.isFiatDisabled;
|
||||
final balanceByAsset = <CryptoCurrency, CurrencyPickerBalance>{
|
||||
for (final r in widget.sendViewModel.balanceViewModel.formattedBalances)
|
||||
r.asset: CurrencyPickerBalance(
|
||||
amount: '${r.availableBalance} ${r.asset.title}',
|
||||
fiat: isFiatDisabled
|
||||
? null
|
||||
: '${r.fiatAvailableBalanceRaw} ${r.fiatCurrencyTicker}',
|
||||
fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
|
||||
),
|
||||
};
|
||||
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: widget.sendViewModel.currencies,
|
||||
selected: widget.sendViewModel.selectedCryptoCurrency,
|
||||
filterByNetwork: widget.sendViewModel.walletType,
|
||||
balanceByAsset: balanceByAsset,
|
||||
symbolResolver: widget.sendViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
onSelected: (currency) {
|
||||
widget.sendViewModel.selectedCryptoCurrency = currency;
|
||||
output.calculateEstimatedFee();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -20,8 +20,9 @@ import 'package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
@@ -1213,9 +1214,11 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
}
|
||||
|
||||
void _presentCurrencyPicker() {
|
||||
final currencies = widget.isReceiverCard
|
||||
final rawCurrencies = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveCurrencies
|
||||
: widget.exchangeViewModel.depositCurrencies;
|
||||
final currencies = rawCurrencies.whereType<CryptoCurrency>().toList();
|
||||
appendEvmDefaultTokens(currencies);
|
||||
if (widget.exchangeViewModel.wallet.type == WalletType.bitcoin) {
|
||||
currencies.sort((a, b) {
|
||||
if (a == CryptoCurrency.btcln) return -1;
|
||||
@@ -1224,20 +1227,18 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
});
|
||||
}
|
||||
|
||||
showPopUp<void>(
|
||||
final selected = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveCurrency
|
||||
: widget.exchangeViewModel.depositCurrency;
|
||||
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
key: ValueKey('send_page_currency_picker_dialog_button_key'),
|
||||
selectedAtIndex: currencies.indexOf(widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveCurrency
|
||||
: widget.exchangeViewModel.depositCurrency),
|
||||
args: CurrencyPickerArgs(
|
||||
items: currencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency cur) async {
|
||||
if (cur is CryptoCurrency) {
|
||||
widget.onCurrencySelected(cur);
|
||||
}
|
||||
},
|
||||
selected: selected,
|
||||
recentsSource: RecentsSource.trades,
|
||||
onSelected: widget.onCurrencySelected,
|
||||
symbolResolver: widget.exchangeViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChainChipStrip extends StatelessWidget {
|
||||
const ChainChipStrip({
|
||||
super.key,
|
||||
required this.walletTypes,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<WalletType> walletTypes;
|
||||
final WalletType? selected;
|
||||
final ValueChanged<WalletType?> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: 48,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
children: [
|
||||
_ChainChip(
|
||||
label: S.of(context).picker_chip_all,
|
||||
isSelected: selected == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
for (final type in walletTypes)
|
||||
_ChainChip(
|
||||
label: walletTypeToString(type),
|
||||
isSelected: selected == type,
|
||||
onTap: () => onSelected(type),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChainChip extends StatelessWidget {
|
||||
const _ChainChip({
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? colors.primary : colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: isSelected ? 14 : 12,
|
||||
letterSpacing: -0.07,
|
||||
color: isSelected ? colors.onPrimary : colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
String chainNameForCurrency(CryptoCurrency c) {
|
||||
if (c == CryptoCurrency.btcln) return 'Lightning';
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
return wt != null ? walletTypeToString(wt) : (c.tag ?? '');
|
||||
}
|
||||
|
||||
final Set<String> _stablecoinSymbols = {
|
||||
for (final c in CryptoCurrency.all)
|
||||
if (c.groups.contains(CurrencyGroups.stablecoin)) c.title.toUpperCase(),
|
||||
};
|
||||
|
||||
bool isTrustedStablecoin(CryptoCurrency c) =>
|
||||
(c.groups.contains(CurrencyGroups.stablecoin) ||
|
||||
_stablecoinSymbols.contains(c.title.toUpperCase())) && !c.isPotentialScam;
|
||||
|
||||
const _kEvmDefaultTokenNatives = <CryptoCurrency>[
|
||||
CryptoCurrency.baseEth,
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.maticpoly,
|
||||
];
|
||||
|
||||
void appendEvmDefaultTokens(List<CryptoCurrency> into) {
|
||||
if (evm == null) return;
|
||||
String keyFor(CryptoCurrency c) {
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
final chain = wt?.toString() ?? (c.tag ?? '').toUpperCase();
|
||||
return '${c.title.toUpperCase()}|$chain';
|
||||
}
|
||||
|
||||
final seen = <String>{for (final c in into) keyFor(c)};
|
||||
for (final native in _kEvmDefaultTokenNatives) {
|
||||
final chainId = getChainIdByCryptoCurrency(native);
|
||||
if (chainId == null) continue;
|
||||
for (final t in evm!.getDefaultTokensByChainId(chainId)) {
|
||||
if (seen.add(keyFor(t))) into.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CurrencyPickerBalance {
|
||||
const CurrencyPickerBalance(
|
||||
{required this.amount, this.fiat, this.fiatValue});
|
||||
|
||||
final String amount;
|
||||
final String? fiat;
|
||||
final double? fiatValue;
|
||||
}
|
||||
|
||||
enum RecentsSource { none, trades, orders }
|
||||
|
||||
class CurrencyPickerArgs {
|
||||
const CurrencyPickerArgs({
|
||||
this.selected,
|
||||
required this.items,
|
||||
this.balanceByAsset,
|
||||
this.filterByNetwork,
|
||||
required this.onSelected,
|
||||
required this.symbolResolver,
|
||||
this.recentsSource = RecentsSource.none,
|
||||
});
|
||||
|
||||
final CryptoCurrency? selected;
|
||||
final List<CryptoCurrency> items;
|
||||
final WalletType? filterByNetwork;
|
||||
final void Function(CryptoCurrency) onSelected;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset;
|
||||
final RecentsSource recentsSource;
|
||||
|
||||
bool get isPreFiltered => filterByNetwork != null;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerListContainer extends StatelessWidget {
|
||||
const CurrencyPickerListContainer({super.key, required this.rows});
|
||||
|
||||
final List<Widget> rows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
children.add(rows[i]);
|
||||
if (i != rows.length - 1) {
|
||||
children.add(Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colors.surfaceContainerHigh,
|
||||
indent: 56,
|
||||
endIndent: 24,
|
||||
));
|
||||
}
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: children),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerRow extends StatelessWidget {
|
||||
const CurrencyPickerRow({
|
||||
super.key,
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.trailing,
|
||||
required this.onTap,
|
||||
this.chainPillLabel,
|
||||
this.chainBadgePath,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final String? chainPillLabel;
|
||||
final String? chainBadgePath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
_IconWithBadge(currency: currency, badgePath: chainBadgePath),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
currency.fullName ?? currency.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chainPillLabel != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
chainPillLabel!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
trailing,
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: isSelected ? colors.primary : colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconWithBadge extends StatelessWidget {
|
||||
const _IconWithBadge({required this.currency, required this.badgePath});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String? badgePath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 32,
|
||||
height: 32,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.length >= 2 ? currency.title.substring(0, 2) : currency.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (badgePath == null) return icon;
|
||||
return SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 2,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: badgePath,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 13,
|
||||
height: 13,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerSearchField extends StatelessWidget {
|
||||
const CurrencyPickerSearchField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
border: Border.all(color: colors.surfaceContainer, width: 1),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: colors.primary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.text.isNotEmpty)
|
||||
InkWell(
|
||||
onTap: controller.clear,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool currencyMatchesQuery(CryptoCurrency c, String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final q = query.toLowerCase();
|
||||
return c.title.toLowerCase().contains(q) ||
|
||||
c.name.toLowerCase().contains(q) ||
|
||||
(c.fullName?.toLowerCase().contains(q) ?? false);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/multi_network_currency_picker.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/single_network_currency_picker.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerSheet extends StatelessWidget {
|
||||
const CurrencyPickerSheet({super.key, required this.args});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required CurrencyPickerArgs args,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => CurrencyPickerSheet(args: args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_asset,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: args.isPreFiltered
|
||||
? SingleNetworkCurrencyPicker(args: args)
|
||||
: MultiNetworkCurrencyPicker(args: args),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_search_result.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencyPickerSheet extends StatefulWidget {
|
||||
const FiatCurrencyPickerSheet({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
this.cryptoOption,
|
||||
this.onCryptoSelected,
|
||||
});
|
||||
|
||||
final Object? selected;
|
||||
final ValueChanged<FiatCurrency> onSelected;
|
||||
final CryptoCurrency? cryptoOption;
|
||||
final ValueChanged<CryptoCurrency>? onCryptoSelected;
|
||||
|
||||
static const _popularOrder = <FiatCurrency>[
|
||||
FiatCurrency.usd,
|
||||
FiatCurrency.eur,
|
||||
FiatCurrency.gbp,
|
||||
FiatCurrency.jpy,
|
||||
FiatCurrency.cny,
|
||||
];
|
||||
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required Object? selected,
|
||||
required ValueChanged<FiatCurrency> onSelected,
|
||||
CryptoCurrency? cryptoOption,
|
||||
ValueChanged<CryptoCurrency>? onCryptoSelected,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => FiatCurrencyPickerSheet(
|
||||
selected: selected,
|
||||
onSelected: onSelected,
|
||||
cryptoOption: cryptoOption,
|
||||
onCryptoSelected: onCryptoSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<FiatCurrencyPickerSheet> createState() => _FiatCurrencyPickerSheetState();
|
||||
}
|
||||
|
||||
class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late final List<FiatCurrency> _allSorted = [...FiatCurrency.all]..sort(
|
||||
(a, b) => a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase()),
|
||||
);
|
||||
|
||||
bool get _isSearching => _searchController.text.trim().isNotEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _matches(FiatCurrency c, String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final q = query.toLowerCase();
|
||||
return c.fullName.toLowerCase().contains(q) || c.title.toLowerCase().contains(q);
|
||||
}
|
||||
|
||||
void _selectFiatCurrency(FiatCurrency c) {
|
||||
widget.onSelected(c);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
void _selectCryptoCurrency(CryptoCurrency c) {
|
||||
widget.onCryptoSelected?.call(c);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final selected = widget.selected;
|
||||
final selectedFiat = selected is FiatCurrency ? selected : null;
|
||||
final popular = FiatCurrencyPickerSheet._popularOrder
|
||||
.where((c) => c != selectedFiat)
|
||||
.toList(growable: false);
|
||||
final query = _searchController.text.trim();
|
||||
final filteredAll = _allSorted.where((c) => _matches(c, query)).toList(growable: false);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_fiat_currency_title,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: _isSearching
|
||||
? FiatCurrencySearchResults(
|
||||
items: filteredAll,
|
||||
onSelected: _selectFiatCurrency,
|
||||
selected: selectedFiat,
|
||||
)
|
||||
: FiatCurrencyPickerBody(
|
||||
selected: selected,
|
||||
cryptoOption: widget.cryptoOption,
|
||||
popular: popular,
|
||||
all: filteredAll,
|
||||
onSelectedFiat: _selectFiatCurrency,
|
||||
onSelectedCrypto: _selectCryptoCurrency,
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FiatCurrencyPickerBody extends StatelessWidget {
|
||||
const FiatCurrencyPickerBody({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.cryptoOption,
|
||||
required this.popular,
|
||||
required this.all,
|
||||
required this.onSelectedFiat,
|
||||
required this.onSelectedCrypto,
|
||||
});
|
||||
|
||||
final Object? selected;
|
||||
final CryptoCurrency? cryptoOption;
|
||||
final List<FiatCurrency> popular;
|
||||
final List<FiatCurrency> all;
|
||||
final void Function(FiatCurrency) onSelectedFiat;
|
||||
final void Function(CryptoCurrency) onSelectedCrypto;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedFiat = selected is FiatCurrency ? selected as FiatCurrency : null;
|
||||
final selectedCrypto = selected is CryptoCurrency ? selected as CryptoCurrency : null;
|
||||
final showCryptoSection = cryptoOption != null && selectedCrypto == null;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (selectedFiat != null || selectedCrypto != null) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_selected),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
if (selectedFiat != null)
|
||||
FiatCurrencyRow(
|
||||
currency: selectedFiat,
|
||||
isSelected: true,
|
||||
onTap: () => onSelectedFiat(selectedFiat),
|
||||
),
|
||||
if (selectedCrypto != null)
|
||||
_CryptoOptionRow(
|
||||
currency: selectedCrypto,
|
||||
isSelected: true,
|
||||
onTap: () => onSelectedCrypto(selectedCrypto),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (showCryptoSection) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_crypto),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
_CryptoOptionRow(
|
||||
currency: cryptoOption!,
|
||||
isSelected: false,
|
||||
onTap: () => onSelectedCrypto(cryptoOption!),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (popular.isNotEmpty) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_popular),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in popular)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: false,
|
||||
onTap: () => onSelectedFiat(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_SectionHeader(label: S.of(context).picker_section_all_currencies),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in all)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: c == selectedFiat,
|
||||
onTap: () => onSelectedFiat(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CryptoOptionRow extends StatelessWidget {
|
||||
const _CryptoOptionRow({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName ?? currency.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currency.title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check, size: 20, color: colors.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencyRow extends StatelessWidget {
|
||||
const FiatCurrencyRow({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final FiatCurrency currency;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/images/flags/${currency.countryCode}.png',
|
||||
width: 28,
|
||||
height: 20,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: Container(
|
||||
width: 28,
|
||||
height: 20,
|
||||
color: colors.surfaceContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currency.title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check, size: 20, color: colors.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencySearchResults extends StatelessWidget {
|
||||
const FiatCurrencySearchResults({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
required this.selected,
|
||||
});
|
||||
|
||||
final List<FiatCurrency> items;
|
||||
final void Function(FiatCurrency) onSelected;
|
||||
final FiatCurrency? selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in items)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: c == selected,
|
||||
onTap: () => onSelected(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/chain_chip_strip.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_recents_loader.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_section_header.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/pill_grid.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/select_network_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultiNetworkCurrencyPicker extends StatefulWidget {
|
||||
const MultiNetworkCurrencyPicker({super.key, required this.args});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
@override
|
||||
State<MultiNetworkCurrencyPicker> createState() => _MultiNetworkCurrencyPickerState();
|
||||
}
|
||||
|
||||
class _MultiNetworkCurrencyPickerState extends State<MultiNetworkCurrencyPicker> {
|
||||
bool _recentsLoaded = false;
|
||||
WalletType? _selectedNetwork;
|
||||
List<CryptoCurrency> _recents = const [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
bool get _isSearching => _searchController.text.trim().isNotEmpty;
|
||||
|
||||
late final List<WalletType> _networks = _computeNetworks();
|
||||
|
||||
List<WalletType> _computeNetworks() {
|
||||
final counts = <WalletType, int>{};
|
||||
for (final currency in widget.args.items) {
|
||||
final walletType = cryptoCurrencyOrTokenToWalletType(currency);
|
||||
if (walletType == null) continue;
|
||||
counts[walletType] = (counts[walletType] ?? 0) + 1;
|
||||
}
|
||||
return counts.entries.where((e) => e.value >= 2).map((e) => e.key).toList(growable: false);
|
||||
}
|
||||
|
||||
Set<CryptoCurrency> get _natives =>
|
||||
{for (final walletType in availableWalletTypes) walletTypeToCryptoCurrency(walletType)};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
_loadRecents();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadRecents() async {
|
||||
try {
|
||||
final recents = await PickerRecentsLoader.load(
|
||||
source: widget.args.recentsSource,
|
||||
visibleItems: widget.args.items,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_recents = recents;
|
||||
_recentsLoaded = true;
|
||||
});
|
||||
} catch (e, s) {
|
||||
printV('load picker recents failed: $e\n$s');
|
||||
if (!mounted) return;
|
||||
setState(() => _recentsLoaded = true);
|
||||
}
|
||||
}
|
||||
|
||||
bool _matchesNetwork(CryptoCurrency c) {
|
||||
if (_selectedNetwork == null) return true;
|
||||
return cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork;
|
||||
}
|
||||
|
||||
List<CryptoCurrency> get _visibleItems {
|
||||
final query = _searchController.text.trim();
|
||||
return widget.args.items
|
||||
.where(_hasSupportedChain)
|
||||
.where((c) => currencyMatchesQuery(c, query) && _matchesNetwork(c))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _hasSupportedChain(CryptoCurrency c) {
|
||||
if (c.tag == null) return true;
|
||||
return cryptoCurrencyOrTokenToWalletType(c) != null;
|
||||
}
|
||||
|
||||
void _selectCurrency(CryptoCurrency currency) {
|
||||
widget.args.onSelected(currency);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
void _onStablecoinPillTapped(CryptoCurrency tapped) {
|
||||
var variants = widget.args.items
|
||||
.where((c) =>
|
||||
isTrustedStablecoin(c) &&
|
||||
c.title.toUpperCase() == tapped.title.toUpperCase() &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) != null)
|
||||
.toList();
|
||||
|
||||
if (_selectedNetwork != null) {
|
||||
final filtered = variants
|
||||
.where((c) => cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork)
|
||||
.toList(growable: false);
|
||||
if (filtered.isNotEmpty) variants = filtered;
|
||||
}
|
||||
|
||||
if (variants.length <= 1) {
|
||||
_selectCurrency(variants.isNotEmpty ? variants.first : tapped);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder<void>(
|
||||
opaque: true,
|
||||
transitionDuration: const Duration(milliseconds: 250),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (_, __, ___) => SelectNetworkPage(
|
||||
assetTitle: tapped.title,
|
||||
assetFullName: tapped.fullName,
|
||||
assetIconPath: tapped.iconPath,
|
||||
variants: variants,
|
||||
onSelected: widget.args.onSelected,
|
||||
),
|
||||
transitionsBuilder: (_, animation, __, child) => SlideTransition(
|
||||
position: animation.drive(
|
||||
Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero)
|
||||
.chain(CurveTween(curve: Curves.easeInOut)),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (_networks.length > 1)
|
||||
ChainChipStrip(
|
||||
walletTypes: _networks,
|
||||
selected: _selectedNetwork,
|
||||
onSelected: (network) => setState(() => _selectedNetwork = network),
|
||||
),
|
||||
Expanded(
|
||||
child: _MultiNetworkPickerBody(
|
||||
items: _visibleItems,
|
||||
isSearching: _isSearching,
|
||||
recents: _recents,
|
||||
recentsLoaded: _recentsLoaded,
|
||||
natives: _natives,
|
||||
selected: widget.args.selected,
|
||||
symbolResolver: widget.args.symbolResolver,
|
||||
onSelect: _selectCurrency,
|
||||
onStablecoinTap: _onStablecoinPillTapped,
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MultiNetworkPickerBody extends StatefulWidget {
|
||||
const _MultiNetworkPickerBody({
|
||||
required this.items,
|
||||
required this.isSearching,
|
||||
required this.recents,
|
||||
required this.recentsLoaded,
|
||||
required this.natives,
|
||||
required this.selected,
|
||||
required this.symbolResolver,
|
||||
required this.onSelect,
|
||||
required this.onStablecoinTap,
|
||||
});
|
||||
|
||||
final bool isSearching;
|
||||
final bool recentsLoaded;
|
||||
final CryptoCurrency? selected;
|
||||
final List<CryptoCurrency> items;
|
||||
final List<CryptoCurrency> recents;
|
||||
final Set<CryptoCurrency> natives;
|
||||
final void Function(CryptoCurrency) onSelect;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final void Function(CryptoCurrency) onStablecoinTap;
|
||||
|
||||
@override
|
||||
State<_MultiNetworkPickerBody> createState() => _MultiNetworkPickerBodyState();
|
||||
}
|
||||
|
||||
class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> {
|
||||
static const int _previewCount = 3;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _moreCryptosSectionExpanded = false;
|
||||
bool _xstocksSectionExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final selected = widget.selected;
|
||||
if (selected == null) return;
|
||||
|
||||
final moreCryptos = _computeMoreCryptosSection(widget.items);
|
||||
final xstocks = _computeXstocksSection(widget.items);
|
||||
|
||||
if (moreCryptos.indexOf(selected) >= _previewCount) {
|
||||
_moreCryptosSectionExpanded = true;
|
||||
}
|
||||
|
||||
if (xstocks.indexOf(selected) >= _previewCount) {
|
||||
_xstocksSectionExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<CryptoCurrency> _computeMoreCryptosSection(List<CryptoCurrency> from) {
|
||||
final list = from
|
||||
.where((c) =>
|
||||
!widget.natives.contains(c) &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) == null &&
|
||||
c.tag == null &&
|
||||
!c.groups.contains(CurrencyGroups.stablecoin) &&
|
||||
!c.groups.contains(CurrencyGroups.tokenizedStock))
|
||||
.toList();
|
||||
|
||||
list.sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo(
|
||||
(b.fullName ?? b.title).toLowerCase(),
|
||||
));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
List<CryptoCurrency> _computeXstocksSection(List<CryptoCurrency> from) {
|
||||
return from
|
||||
.where((c) => c.groups.contains(CurrencyGroups.tokenizedStock))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = widget.items;
|
||||
final isSearching = widget.isSearching;
|
||||
final recents = widget.recents;
|
||||
final recentsLoaded = widget.recentsLoaded;
|
||||
final natives = widget.natives;
|
||||
final selected = widget.selected;
|
||||
final symbolResolver = widget.symbolResolver;
|
||||
final onSelect = widget.onSelect;
|
||||
final onStablecoinTap = widget.onStablecoinTap;
|
||||
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isSearching) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
primary: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in items)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: selected != null && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final visibleRecents = recents.where(items.contains).toList(growable: false);
|
||||
|
||||
final seenStablecoinTitles = <String>{};
|
||||
final stablecoins = items
|
||||
.where((c) => isTrustedStablecoin(c) && seenStablecoinTitles.add(c.title.toUpperCase()))
|
||||
.toList(growable: false);
|
||||
|
||||
final cryptocurrencies = items.where(natives.contains).toList(growable: false);
|
||||
final moreCryptos = _computeMoreCryptosSection(items);
|
||||
final xstocks = _computeXstocksSection(items);
|
||||
final allAssets = [...items]..sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo(
|
||||
(b.fullName ?? b.title).toLowerCase(),
|
||||
));
|
||||
|
||||
final section = _selectedSection(
|
||||
recents: visibleRecents,
|
||||
stablecoins: stablecoins,
|
||||
cryptocurrencies: cryptocurrencies,
|
||||
moreCryptos: moreCryptos,
|
||||
xstocks: xstocks,
|
||||
);
|
||||
|
||||
final moreCryptosVisible = _moreCryptosSectionExpanded
|
||||
? moreCryptos
|
||||
: moreCryptos.take(_previewCount).toList(growable: false);
|
||||
final xstocksVisible =
|
||||
_xstocksSectionExpanded ? xstocks : xstocks.take(_previewCount).toList(growable: false);
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
primary: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (recentsLoaded && visibleRecents.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_recents,
|
||||
child: _RecentsRow(
|
||||
items: visibleRecents,
|
||||
selected: section == _SelSection.recents ? selected : null,
|
||||
symbolResolver: symbolResolver,
|
||||
onTap: onSelect,
|
||||
),
|
||||
),
|
||||
if (stablecoins.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_stablecoins,
|
||||
child: PillGrid(
|
||||
items: stablecoins,
|
||||
selected: section == _SelSection.stablecoins ? selected : null,
|
||||
onTap: onStablecoinTap,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
),
|
||||
if (cryptocurrencies.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_cryptocurrencies,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in cryptocurrencies)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.cryptocurrencies && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (moreCryptos.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_more_cryptocurrencies,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in moreCryptosVisible)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.moreCryptocurrencies && selected == item,
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
if (moreCryptos.length > _previewCount)
|
||||
_SeeAllRow(
|
||||
expanded: _moreCryptosSectionExpanded,
|
||||
onTap: () => setState(
|
||||
() => _moreCryptosSectionExpanded = !_moreCryptosSectionExpanded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (xstocks.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_tokenized_stocks,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in xstocksVisible)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.xstocks && selected == item,
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
),
|
||||
if (xstocks.length > _previewCount)
|
||||
_SeeAllRow(
|
||||
expanded: _xstocksSectionExpanded,
|
||||
onTap: () =>
|
||||
setState(() => _xstocksSectionExpanded = !_xstocksSectionExpanded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (allAssets.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_all_assets,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in allAssets)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.allAssets && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _chainPillLabelFor(CryptoCurrency c) {
|
||||
if (c == CryptoCurrency.btcln) return _shortChainLabel(c);
|
||||
if (_isL2NativeEth(c)) return _shortChainLabel(c);
|
||||
if (widget.natives.contains(c)) return null;
|
||||
if (cryptoCurrencyOrTokenToWalletType(c) == null) return null;
|
||||
return _shortChainLabel(c);
|
||||
}
|
||||
|
||||
String _shortChainLabel(CryptoCurrency c) {
|
||||
if (cryptoCurrencyOrTokenToWalletType(c) == WalletType.bsc) return 'BSC';
|
||||
return chainNameForCurrency(c);
|
||||
}
|
||||
|
||||
String? _chainBadgePathFor(CryptoCurrency c) {
|
||||
if (_isL2NativeEth(c)) return c.chainIconPath;
|
||||
if (widget.natives.contains(c)) return null;
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
if (wt == null) return null;
|
||||
return c.chainIconPath ?? walletTypeToCryptoCurrency(wt).chainIconPath;
|
||||
}
|
||||
|
||||
bool _isL2NativeEth(CryptoCurrency c) =>
|
||||
c == CryptoCurrency.arbEth || c == CryptoCurrency.baseEth;
|
||||
|
||||
_SelSection? _selectedSection({
|
||||
required List<CryptoCurrency> recents,
|
||||
required List<CryptoCurrency> stablecoins,
|
||||
required List<CryptoCurrency> cryptocurrencies,
|
||||
required List<CryptoCurrency> moreCryptos,
|
||||
required List<CryptoCurrency> xstocks,
|
||||
}) {
|
||||
final s = widget.selected;
|
||||
if (s == null) return null;
|
||||
if (recents.contains(s)) return _SelSection.recents;
|
||||
if (stablecoins.any((c) => c.title.toUpperCase() == s.title.toUpperCase())) {
|
||||
return _SelSection.stablecoins;
|
||||
}
|
||||
if (cryptocurrencies.contains(s)) return _SelSection.cryptocurrencies;
|
||||
if (moreCryptos.contains(s)) return _SelSection.moreCryptocurrencies;
|
||||
if (xstocks.contains(s)) return _SelSection.xstocks;
|
||||
return _SelSection.allAssets;
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerSection extends StatelessWidget {
|
||||
const _PickerSection({required this.title, required this.child});
|
||||
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PickerSectionHeader(title: title),
|
||||
child,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SymbolTrailing extends StatelessWidget {
|
||||
const _SymbolTrailing({
|
||||
required this.currency,
|
||||
required this.symbolResolver,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
symbolResolver(currency),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeeAllRow extends StatelessWidget {
|
||||
const _SeeAllRow({
|
||||
required this.expanded,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool expanded;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final label = expanded ? S.of(context).picker_show_less : S.of(context).picker_see_all;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentsRow extends StatelessWidget {
|
||||
const _RecentsRow({
|
||||
required this.items,
|
||||
required this.selected,
|
||||
required this.symbolResolver,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final List<CryptoCurrency> items;
|
||||
final CryptoCurrency? selected;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final void Function(CryptoCurrency) onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final item = items[i];
|
||||
return _RecentPill(
|
||||
currency: item,
|
||||
label: symbolResolver(item),
|
||||
isSelected: selected != null && selected == item,
|
||||
onTap: () => onTap(item),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentPill extends StatelessWidget {
|
||||
const _RecentPill({
|
||||
required this.currency,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
border: isSelected ? Border.all(color: colors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _SelSection {
|
||||
recents,
|
||||
stablecoins,
|
||||
cryptocurrencies,
|
||||
moreCryptocurrencies,
|
||||
xstocks,
|
||||
allAssets,
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/order/order.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
class PickerRecentsLoader {
|
||||
const PickerRecentsLoader._();
|
||||
|
||||
static const int defaultLimit = 6;
|
||||
|
||||
static final Set<TradeState> _executedTradeStates = {
|
||||
TradeState.complete,
|
||||
TradeState.completed,
|
||||
TradeState.finished,
|
||||
TradeState.success,
|
||||
TradeState.settled,
|
||||
TradeState.traded,
|
||||
};
|
||||
|
||||
static Future<List<CryptoCurrency>> load({
|
||||
required RecentsSource source,
|
||||
required List<CryptoCurrency> visibleItems,
|
||||
int limit = defaultLimit,
|
||||
}) async {
|
||||
switch (source) {
|
||||
case RecentsSource.none:
|
||||
return const [];
|
||||
case RecentsSource.trades:
|
||||
return _fromTrades(visibleItems, limit);
|
||||
case RecentsSource.orders:
|
||||
return _fromOrders(visibleItems, limit);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> _fromTrades(
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) async {
|
||||
final trades = await Trade.getAll();
|
||||
final ordered = trades
|
||||
.where((t) => _executedTradeStates.contains(t.state))
|
||||
.toList()
|
||||
..sort((a, b) => (b.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.compareTo(a.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0)));
|
||||
|
||||
return _collect(
|
||||
ordered.expand((t) => [t.from, t.to]),
|
||||
visibleItems,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> _fromOrders(
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) async {
|
||||
final orders = Hive.box<Order>(Order.boxName).values.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
return _collect(
|
||||
orders.expand((o) => [_tryResolve(o.from), _tryResolve(o.to)]),
|
||||
visibleItems,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
static List<CryptoCurrency> _collect(
|
||||
Iterable<CryptoCurrency?> candidates,
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) {
|
||||
final visibleSet = visibleItems.toSet();
|
||||
final seen = <CryptoCurrency>{};
|
||||
final collected = <CryptoCurrency>[];
|
||||
|
||||
for (final candidate in candidates) {
|
||||
if (candidate == null) continue;
|
||||
if (!visibleSet.contains(candidate)) continue;
|
||||
if (!seen.add(candidate)) continue;
|
||||
collected.add(candidate);
|
||||
if (collected.length >= limit) break;
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
static CryptoCurrency? _tryResolve(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
try {
|
||||
return CryptoCurrency.fromString(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PickerSectionHeader extends StatelessWidget {
|
||||
const PickerSectionHeader({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PillGrid extends StatelessWidget {
|
||||
const PillGrid({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.symbolResolver,
|
||||
});
|
||||
|
||||
final List<CryptoCurrency> items;
|
||||
final CryptoCurrency? selected;
|
||||
final ValueChanged<CryptoCurrency> onTap;
|
||||
final String Function(CryptoCurrency c) symbolResolver;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const gap = 8.0;
|
||||
final cardWidth = (constraints.maxWidth - gap) / 2;
|
||||
return Wrap(
|
||||
spacing: gap,
|
||||
runSpacing: gap,
|
||||
children: [
|
||||
for (final item in items)
|
||||
SizedBox(
|
||||
width: cardWidth,
|
||||
child: _PillCard(
|
||||
currency: item,
|
||||
isSelected: selected != null && selected == item,
|
||||
label: symbolResolver(item),
|
||||
onTap: () => onTap(item),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PillCard extends StatelessWidget {
|
||||
const _PillCard({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
border: isSelected ? Border.all(color: colors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SelectNetworkPage extends StatelessWidget {
|
||||
const SelectNetworkPage({
|
||||
super.key,
|
||||
required this.assetTitle,
|
||||
required this.assetFullName,
|
||||
required this.assetIconPath,
|
||||
required this.variants,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final String assetTitle;
|
||||
final String? assetFullName;
|
||||
final String? assetIconPath;
|
||||
final List<CryptoCurrency> variants;
|
||||
final ValueChanged<CryptoCurrency> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_network_title,
|
||||
leadingIcon: const Icon(Icons.arrow_back),
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CakeImageWidget(
|
||||
imageUrl: assetIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
assetTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_network_subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final variant in variants)
|
||||
_NetworkRow(
|
||||
variant: variant,
|
||||
onTap: () {
|
||||
final nav = Navigator.of(context);
|
||||
onSelected(variant);
|
||||
if (nav.canPop()) nav.pop();
|
||||
if (nav.canPop()) nav.pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkRow extends StatelessWidget {
|
||||
const _NetworkRow({required this.variant, required this.onTap});
|
||||
|
||||
final CryptoCurrency variant;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(variant);
|
||||
final networkIconPath =
|
||||
wt != null ? getCryptoCurrencyIconForWalletListItem(wt) : variant.iconPath;
|
||||
final networkName = chainNameForCurrency(variant);
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_section_header.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SingleNetworkCurrencyPicker extends StatefulWidget {
|
||||
const SingleNetworkCurrencyPicker({super.key, required this.args});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
@override
|
||||
State<SingleNetworkCurrencyPicker> createState() => _SingleNetworkCurrencyPickerState();
|
||||
}
|
||||
|
||||
class _SingleNetworkCurrencyPickerState extends State<SingleNetworkCurrencyPicker> {
|
||||
CurrencyPickerArgs get _args => widget.args;
|
||||
WalletType get _network => _args.filterByNetwork!;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _selectCurrency(CryptoCurrency currency) {
|
||||
_args.onSelected(currency);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
double _fiatValueFor(CryptoCurrency c) => _args.balanceByAsset?[c]?.fiatValue ?? 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final native = walletTypeToCryptoCurrency(_network);
|
||||
final query = _searchController.text.trim();
|
||||
final tokens = _args.items
|
||||
.where((c) => c != native)
|
||||
.where((c) => currencyMatchesQuery(c, query))
|
||||
.toList();
|
||||
|
||||
final insertOrder = {for (var i = 0; i < tokens.length; i++) tokens[i]: i};
|
||||
tokens.sort((a, b) {
|
||||
final av = _fiatValueFor(a);
|
||||
final bv = _fiatValueFor(b);
|
||||
if (bv != av) return bv.compareTo(av);
|
||||
return (insertOrder[a] ?? 0).compareTo(insertOrder[b] ?? 0);
|
||||
});
|
||||
final nativeMatches = currencyMatchesQuery(native, query);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: (nativeMatches || tokens.isNotEmpty)
|
||||
? ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (nativeMatches) ...[
|
||||
PickerSectionHeader(title: S.of(context).picker_section_gas_token),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _args.balanceByAsset?[native]),
|
||||
onTap: () => _selectCurrency(native),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (tokens.isNotEmpty) ...[
|
||||
PickerSectionHeader(
|
||||
title: S
|
||||
.of(context)
|
||||
.picker_section_tokens_standard(tokenStandardFor(_network)),
|
||||
),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _args.balanceByAsset?[t]),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BalanceTrailing extends StatelessWidget {
|
||||
const _BalanceTrailing({required this.balance});
|
||||
|
||||
final CurrencyPickerBalance? balance;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final amount = balance?.amount ?? '—';
|
||||
final fiat = balance?.fiat;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (fiat != null && fiat.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
fiat,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class ReceiveAmountModal extends StatefulWidget {
|
||||
const ReceiveAmountModal(
|
||||
@@ -217,35 +217,31 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
|
||||
);
|
||||
}
|
||||
|
||||
void _presentTokenCurrencyPicker(BuildContext context) async {
|
||||
await showPopUp(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: widget.walletAddressListViewModel.tokenCurrencyIndex,
|
||||
items: widget.walletAddressListViewModel.tokenCurrencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: widget.walletAddressListViewModel.setTokenCurrency,
|
||||
),
|
||||
void _presentTokenCurrencyPicker(BuildContext context) {
|
||||
final items = widget.walletAddressListViewModel.tokenCurrencies
|
||||
.whereType<CryptoCurrency>()
|
||||
.toList();
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: items,
|
||||
selected: widget.walletAddressListViewModel.tokenCurrency,
|
||||
onSelected: widget.walletAddressListViewModel.setTokenCurrency,
|
||||
symbolResolver: widget.walletAddressListViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _presentFiatCurrencyPicker(BuildContext context) async {
|
||||
final currs = widget.walletAddressListViewModel.currencies.toList();
|
||||
// the first options should be the crypto and the default fiat currency
|
||||
currs.sort((a, b) {
|
||||
if(a == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return -1;
|
||||
if(b == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
await showPopUp(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: currs.indexOf(widget.walletAddressListViewModel.selectedCurrency),
|
||||
items: currs,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
),
|
||||
void _presentFiatCurrencyPicker(BuildContext context) {
|
||||
final selected = widget.walletAddressListViewModel.selectedCurrency;
|
||||
final cryptoOption =
|
||||
widget.walletAddressListViewModel.currencies.whereType<CryptoCurrency>().firstOrNull;
|
||||
FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: selected,
|
||||
cryptoOption: cryptoOption,
|
||||
onSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
onCryptoSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,22 @@ bool hasTokens(WalletType walletType) {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String tokenStandardFor(WalletType walletType) {
|
||||
switch (walletType) {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.arbitrum:
|
||||
case WalletType.base:
|
||||
return 'ERC-20';
|
||||
case WalletType.bsc:
|
||||
return 'BEP-20';
|
||||
case WalletType.solana:
|
||||
return 'SPL';
|
||||
case WalletType.tron:
|
||||
return 'TRC-20';
|
||||
default:
|
||||
return walletTypeToString(walletType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
@@ -398,6 +401,7 @@ class BuySellPage extends BasePage {
|
||||
showLimitsField: false,
|
||||
currencies: buySellViewModel.fiatCurrencies,
|
||||
onCurrencySelected: (currency) => buySellViewModel.changeFiatCurrency(currency: currency),
|
||||
onTapCurrencyPicker: _presentFiatPicker,
|
||||
imageArrow: Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@@ -435,6 +439,7 @@ class BuySellPage extends BasePage {
|
||||
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
|
||||
currencies: buySellViewModel.cryptoCurrencies,
|
||||
onCurrencySelected: (currency) => buySellViewModel.changeCryptoCurrency(currency: currency),
|
||||
onTapCurrencyPicker: _presentCryptoPicker,
|
||||
imageArrow: Image.asset(
|
||||
'assets/images/arrow_bottom_cake_green.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@@ -504,6 +509,29 @@ class BuySellPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
void _presentFiatPicker(BuildContext context) {
|
||||
FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: buySellViewModel.fiatCurrency,
|
||||
onSelected: (currency) => buySellViewModel.changeFiatCurrency(currency: currency),
|
||||
);
|
||||
}
|
||||
|
||||
void _presentCryptoPicker(BuildContext context) {
|
||||
final items = [...buySellViewModel.cryptoCurrencies];
|
||||
appendEvmDefaultTokens(items);
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: items,
|
||||
selected: buySellViewModel.cryptoCurrency,
|
||||
recentsSource: RecentsSource.orders,
|
||||
onSelected: (currency) => buySellViewModel.changeCryptoCurrency(currency: currency),
|
||||
symbolResolver: buySellViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context,
|
||||
String domain,
|
||||
|
||||
@@ -48,6 +48,7 @@ class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||
this.onPushAddressBookButton,
|
||||
this.onDispose,
|
||||
this.useSatoshis = false,
|
||||
this.onTapCurrencyPicker,
|
||||
required this.cardInstanceName,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -84,6 +85,7 @@ class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||
final String cardInstanceName;
|
||||
final Color fillColor;
|
||||
final bool useSatoshis;
|
||||
final void Function(BuildContext context)? onTapCurrencyPicker;
|
||||
|
||||
@override
|
||||
ExchangeCardState<T> createState() => ExchangeCardState<T>();
|
||||
@@ -447,6 +449,10 @@ class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
if (widget.onTapCurrencyPicker != null) {
|
||||
widget.onTapCurrencyPicker!(context);
|
||||
return;
|
||||
}
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/language_service.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart';
|
||||
import 'package:cake_wallet/entities/sync_status_display_mode.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_theme_choice.dart';
|
||||
@@ -179,35 +179,11 @@ class DisplaySettingsPage extends StatelessWidget {
|
||||
keyValue: "display_settings_fiat_currency",
|
||||
label: S.of(context).settings_currency,
|
||||
options: [_displaySettingsViewModel.fiatCurrency.title],
|
||||
onTap: () async {
|
||||
final items = FiatCurrency.all;
|
||||
|
||||
final selectedAtIndex =
|
||||
items.indexOf(_displaySettingsViewModel.fiatCurrency);
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
items: items,
|
||||
selectedAtIndex: selectedAtIndex,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
onItemSelected: (FiatCurrency currency) {
|
||||
_displaySettingsViewModel.setFiatCurrency(currency);
|
||||
},
|
||||
images: FiatCurrency.all
|
||||
.map((e) => Image.asset("assets/images/flags/${e.countryCode}.png"))
|
||||
.toList(),
|
||||
hintText: S.of(context).search_currency,
|
||||
isGridView: true,
|
||||
matchingCriteria: (FiatCurrency currency, String searchText) {
|
||||
return currency.title.toLowerCase().contains(searchText) ||
|
||||
currency.fullName.toLowerCase().contains(searchText);
|
||||
},
|
||||
isSeparated: false,
|
||||
|
||||
),
|
||||
);
|
||||
}),
|
||||
onTap: () => FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: _displaySettingsViewModel.fiatCurrency,
|
||||
onSelected: _displaySettingsViewModel.setFiatCurrency,
|
||||
)),
|
||||
ListItemSelector(
|
||||
keyValue: "display_settings_language",
|
||||
label: S.of(context).settings_change_language,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:cake_wallet/buy/buy_quote.dart';
|
||||
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/payment_method.dart';
|
||||
import 'package:cake_wallet/buy/sell_buy_states.dart';
|
||||
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:cake_wallet/core/selectable_option.dart';
|
||||
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
@@ -87,6 +88,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
|
||||
|
||||
final AppStore _appStore;
|
||||
|
||||
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
|
||||
|
||||
Quote? bestRateQuote;
|
||||
|
||||
Quote? selectedQuote;
|
||||
|
||||
@@ -38,11 +38,9 @@ import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/exchange/trade_request.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
@@ -270,17 +268,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
|
||||
void showFiatCurrencyPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
key: ValueKey('send_page_currency_picker_dialog_button_key'),
|
||||
selectedAtIndex: FiatCurrency.all.indexOf(fiat),
|
||||
items: FiatCurrency.all,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency cur) async {
|
||||
_settingsStore.fiatCurrency = (cur as FiatCurrency);
|
||||
},
|
||||
),
|
||||
selected: fiat,
|
||||
onSelected: (cur) => _settingsStore.fiatCurrency = cur,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:core';
|
||||
import 'dart:developer' as dev;
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
||||
import 'package:cake_wallet/decred/decred.dart';
|
||||
@@ -167,6 +168,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
bool get hasPayjoin =>
|
||||
wallet.type == WalletType.bitcoin && !isLightning && !isSilentPayments && uri.toString().contains("payjo.in");
|
||||
|
||||
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
|
||||
|
||||
@computed
|
||||
FiatCurrency get fiatCurrency => _appStore.settingsStore.fiatCurrency;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user