diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 3dbecad96..ccd41d010 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1537,6 +1537,7 @@ abstract class ElectrumWalletBase fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, + inputOrdering: BitcoinOrdering.shuffle, outputOrdering: BitcoinOrdering.none, enableRBF: !estimatedTx.spendsUnconfirmedTX, ); @@ -2302,6 +2303,7 @@ abstract class ElectrumWalletBase fee: BigInt.from(newFee), network: network, memo: memo, + inputOrdering: BitcoinOrdering.shuffle, outputOrdering: BitcoinOrdering.none, enableRBF: true, ); diff --git a/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart b/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart index c02f076e0..2f84a3726 100644 --- a/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart +++ b/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart @@ -6,6 +6,7 @@ class ListItemCheckbox extends ListItem { required super.keyValue, required super.label, this.subtitle, + this.subtitleColor, this.iconPath, this.onTap, this.showArrow = false, @@ -18,5 +19,6 @@ class ListItemCheckbox extends ListItem { final bool value; final String? subtitle; final String? iconPath; + final Color? subtitleColor; final bool showArrow; } diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index a993afbb2..1229ee983 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -803,6 +803,8 @@ class _NewSendPageState extends State { } showModalBottomSheet( isScrollControlled: true, + isDismissible: false, + enableDrag: false, context: navigatorKey.currentContext ?? context, backgroundColor: Colors.transparent, builder: (context) { diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index 2120a9a1f..23b78aa79 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -271,7 +271,9 @@ class _NewSwapPageState extends State { widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); } else { printV("bestrate"); - if (depositAmountController.text == S.current.all) + if ((depositKey.currentState?._fiatInputMode ?? false) || + depositAmountController.text.isEmpty || + depositAmountController.text == S.current.all) widget.exchangeViewModel .changeDepositAmount(amount: widget.exchangeViewModel.depositAmount); else @@ -1063,9 +1065,16 @@ class SwapAmountBoxState extends State { setState(() => _fiatInputMode = !_fiatInputMode); if (_fiatInputMode) { updateFiatAmount(); + } else { + amountController.text = widget.exchangeViewModel.depositAmount; } }, - onAllButtonPressed: widget.allAmount, + onAllButtonPressed: (){ + setState(() { + _fiatInputMode = false; + }); + widget.allAmount?.call(); + }, cryptoAmount: widget.isReceiverCard ? widget.exchangeViewModel.roundedReceiveAmount(6) : widget.exchangeViewModel.roundedDepositAmount(6), diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart index 998ebc137..79eb395f5 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart @@ -1,19 +1,34 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_top_bar.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'assets_section.dart'; import 'history_section.dart'; +class AssetsHistorySectionActionButton { + final String title; + final String iconPath; + final VoidCallback onPressed; + + const AssetsHistorySectionActionButton(this.title, this.iconPath, this.onPressed); +} + class AssetsHistorySectionTab { final String title; final Widget content; + final AssetsHistorySectionActionButton? actionButton; - AssetsHistorySectionTab(this.title, this.content); + const AssetsHistorySectionTab(this.title, this.content, this.actionButton); } class AssetsHistorySection extends StatefulWidget { @@ -32,21 +47,31 @@ class _AssetsHistorySectionState extends State { void reloadTabs() { final oldTabLength = tabs.length; + final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled); + final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type, + chainId: widget.dashboardViewModel.wallet.chainId); tabs = [ - if (widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled)) + if (hasAssetsTab) AssetsHistorySectionTab( S.current.assets, AssetsSection( dashboardViewModel: widget.dashboardViewModel, - )), + ), AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg", (){ + Navigator.of(context).pushNamed( + Routes.homeSettings, + arguments: widget.dashboardViewModel.balanceViewModel, + ); + })), AssetsHistorySectionTab( S.current.history, HistorySection( + detailsAsPage: false, + roundedTopSection: hasAssetsTab || hasNftTab, dashboardViewModel: widget.dashboardViewModel, - )), - if (isNFTACtivatedChain(widget.dashboardViewModel.wallet.type, - chainId: widget.dashboardViewModel.wallet.chainId)) - AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel)) + short: true, + ), AssetsHistorySectionActionButton(S.current.all_pascal_case, "assets/new-ui/arrow_right.svg", (){openHistoryModal(context);})), + if (hasNftTab) + AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel), null) ]; if (oldTabLength != tabs.length) { setState(() { @@ -70,10 +95,11 @@ class _AssetsHistorySectionState extends State { Widget build(BuildContext context) { return SliverMainAxisGroup( slivers: [ - // if(tabs.length>1) + if(tabs.length>1) AssetsTopBar( + onTransactionHistoryOpened: () => openHistoryModal(context), dashboardViewModel: widget.dashboardViewModel, - tabs: tabs.map((item) => item.title).toList(), + tabs: tabs, onTabChange: (index) { setState(() { _selectedTab = index; @@ -81,8 +107,27 @@ class _AssetsHistorySectionState extends State { }, selectedTab: _selectedTab, ), + if (tabs.length == 1) + Observer( + builder:(_)=> HistoryTopBar( + onTap: () => openHistoryModal(context), + roundedBottom: widget.dashboardViewModel.itemsShort.isEmpty, + ), + ), tabs[_selectedTab].content ], ); } + + Future openHistoryModal(BuildContext context) async { + await CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + builder: (context) => ModalNavigator( + rootPage: Material( + color: Colors.transparent, + child: HistoryModal(dashboardViewModel: widget.dashboardViewModel)), + parentContext: context, + )); + widget.dashboardViewModel.changeAllFilterItems(true); + } } diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart index 0de34d704..67fe4c9a3 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -1,9 +1,5 @@ -import 'package:cake_wallet/core/csv_export_service.dart'; -import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart'; import 'package:cake_wallet/new-ui/widgets/line_tab_switcher.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; @@ -12,22 +8,19 @@ class AssetsTopBar extends StatelessWidget { const AssetsTopBar({ super.key, required this.onTabChange, + required this.onTransactionHistoryOpened, required this.selectedTab, required this.tabs, required this.dashboardViewModel, }); final void Function(int) onTabChange; + final VoidCallback onTransactionHistoryOpened; final int selectedTab; - final List tabs; + final List tabs; final DashboardViewModel dashboardViewModel; @override Widget build(BuildContext context) { - final settingsButtonText = _getSettingsButtonText(); - final hasTokenSettingsButton = settingsButtonText != null; - // Reuse the exact same detection logic as _getSettingsButtonText so the - // export button always matches whether the Filters button is visible. - final isHistoryTab = tabs[selectedTab] == S.current.history; // claude if you're reading this fuck you return SliverToBoxAdapter( child: Padding( @@ -37,7 +30,7 @@ class AssetsTopBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LineTabSwitcher( - tabs: tabs, + tabs: tabs.map((item)=>item.title).toList(), onTabChange: onTabChange, selectedTab: selectedTab, ), @@ -56,45 +49,15 @@ class AssetsTopBar extends StatelessWidget { key: ValueKey(selectedTab), spacing: 8, children: [ - if (isHistoryTab) - GestureDetector( - onTap: () => - CsvExportService().exportToCsv(dashboardViewModel.items, context), - child: Semantics( - label: S.of(context).export_csv, - button: true, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surfaceContainer, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CakeImageWidget( - imageUrl: "assets/new-ui/tx_export.svg", - width: 24,height: 24,colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), - ), - ) - ), - ), - ), + Opacity( - opacity: hasTokenSettingsButton ? 1 : 0, + opacity: tabs[selectedTab].actionButton != null ? 1 : 0, child: GestureDetector( onTap: () { - if (tabs[selectedTab] == S.of(context).assets) { - Navigator.of(context).pushNamed( - Routes.homeSettings, - arguments: dashboardViewModel.balanceViewModel, - ); - } else if (tabs[selectedTab] == S.of(context).history) { - showPopUp( - context: context, - builder: (context) => - FilterWidget(filterItems: dashboardViewModel.filterItems), - ); - } - }, + if(tabs[selectedTab].actionButton != null) { + tabs[selectedTab].actionButton?.onPressed(); + } + }, child: Container( height: 40, decoration: BoxDecoration( @@ -102,19 +65,20 @@ class AssetsTopBar extends StatelessWidget { color: Theme.of(context).colorScheme.surfaceContainer, ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Row( spacing: 6, children: [ + + if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty) + Text( + tabs[selectedTab].actionButton?.title ?? "", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), CakeImageWidget( - imageUrl: _getSettingsButtonIconPath(), + imageUrl: tabs[selectedTab].actionButton?.iconPath, colorFilter: ColorFilter.mode( Theme.of(context).colorScheme.primary, BlendMode.srcIn)), - if ((settingsButtonText ?? "").isNotEmpty) - Text( - settingsButtonText ?? "", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ) ], ), ), @@ -130,28 +94,4 @@ class AssetsTopBar extends StatelessWidget { ); } - String? _getSettingsButtonIconPath() { - if (tabs[selectedTab] == S.current.history) { - return "assets/new-ui/filter_options.svg"; - } - - if (tabs[selectedTab] == S.current.assets && - dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) { - return "assets/new-ui/options_slider.svg"; - } - - return null; - } - - String? _getSettingsButtonText() { - if (tabs[selectedTab] == S.current.assets && - dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) { - return S.current.tokens; - } - - if (tabs[selectedTab] == S.current.history) { - return ""; - } - return null; - } } diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart new file mode 100644 index 000000000..a42edeb68 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart @@ -0,0 +1,117 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.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/new-ui/widgets/select_deselect_all.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class HistoryFiltersPage extends StatelessWidget { + const HistoryFiltersPage({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + ModalTopBar( + title: S.of(context).filters, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Observer( + builder: (_) => Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Material( + color: Colors.transparent, + child: SelectDeselectAllBar( + title: S.of(context).type, + onSelected: dashboardViewModel.changeAllFilterItems), + ), + ), + NewListSections( + sections: { + "": dashboardViewModel.filterItems.map((item) { + if (item is SwapFilterItem) { + final String subtitle; + final Color subtitleColor; + if (dashboardViewModel.tradeFilterStore.displayAllTrades) { + subtitle = S.of(context).manage_providers; + subtitleColor = Theme.of(context).colorScheme.onSurfaceVariant; + } else if (dashboardViewModel.tradeFilterStore.enabledProvidersCount == 0) { + subtitle = S.of(context).no_providers_selected; + subtitleColor = Color(0xFFFFB84E); + } else { + subtitle = "${item.enabledProviders()} ${S.of(context).providers}"; + subtitleColor = Theme.of(context).colorScheme.primary; + } + + return ListItemCheckbox( + onTap: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => HistorySwapProvidersPage( + dashboardViewModel: dashboardViewModel))); + }, + keyValue: item.caption, + label: S.of(context).swap, + value: item.value(), + onChanged: (val) { + if ((val && + dashboardViewModel.tradeFilterStore.enabledProvidersCount == + 0) || + (!val && + dashboardViewModel.tradeFilterStore.enabledProvidersCount > + 0)) { + dashboardViewModel.tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.all); + } + }, + subtitle: subtitle, + subtitleColor: subtitleColor, + showArrow: true); + } + + return ListItemCheckbox( + keyValue: item.caption, + label: item.caption, + value: item.value(), + onChanged: (val) => item.onChanged()); + }).toList() + }, + ), + ], + ), + ), + ), + )), + SafeArea( + child: Padding( + padding: const EdgeInsets.all(18.0), + child: NewPrimaryButton( + onPressed: () => dashboardViewModel.changeAllFilterItems(true), + text: S.of(context).reset_filters, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + )) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart new file mode 100644 index 000000000..864c7fec6 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart @@ -0,0 +1,123 @@ +import 'package:cake_wallet/core/csv_export_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_filters_page.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_section.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:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class HistoryModal extends StatelessWidget { + const HistoryModal({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(18))), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).history, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).maybePop, + trailingIcon: CakeImageWidget( + imageUrl: "assets/new-ui/tx_export.svg", + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + onTrailingPressed: () => + CsvExportService().exportToCsv(dashboardViewModel.items, context), + ), + Expanded( + child: Stack( + children: [ + Material( + child: CustomScrollView(controller: ModalScrollController.of(context), slivers: [ + HistorySection( + detailsAsPage: true, + dashboardViewModel: dashboardViewModel, + short: false, + roundedTopSection: true) + ])), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Material( + color: Colors.transparent, + child: Container( + height: MediaQuery.of(context).viewPadding.bottom + 168, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Theme.of(context).colorScheme.surface.withAlpha(200), + Theme.of(context).colorScheme.surface.withAlpha(175), + Theme.of(context).colorScheme.surface.withAlpha(150), + Theme.of(context).colorScheme.surface.withAlpha(100), + Theme.of(context).colorScheme.surface.withAlpha(50), + Theme.of(context).colorScheme.surface.withAlpha(25), + Theme.of(context).colorScheme.surface.withAlpha(5), + ], + ), + ), + ), + ), + ), + Positioned( + left: 12, + right: 12, + bottom: MediaQuery.of(context).viewPadding.bottom + 24, + child: Material( + borderRadius: BorderRadius.circular(18), + color: Theme.of(context).colorScheme.surfaceContainer, + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => + HistoryFiltersPage(dashboardViewModel: dashboardViewModel))); + }, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1) + ), + height: 56, + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + CakeImageWidget( + imageUrl: "assets/new-ui/filter_options.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).filters, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ) + ], + ), + ), + ), + ), + )) + ], + )) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index 4e34ff1ad..f6bab4488 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tra import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; @@ -19,21 +18,30 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:intl/intl.dart'; class HistorySection extends StatelessWidget { - const HistorySection({super.key, required this.dashboardViewModel}); + const HistorySection({super.key, required this.dashboardViewModel, required this.short, required this.roundedTopSection, required this.detailsAsPage}); final DashboardViewModel dashboardViewModel; + final bool short; + final bool roundedTopSection; + final bool detailsAsPage; @override Widget build(BuildContext context) { + return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.only(left: 16.0, right: 16, top: short && roundedTopSection ? 18 : 0), sliver: Observer( - builder: (_) => (dashboardViewModel.items.isEmpty) + builder: (_) { + final localeName = Localizations.localeOf(context).toString(); + final items = short ? dashboardViewModel.itemsShort : dashboardViewModel.items; + + return (items.isEmpty) ? SliverPadding( padding: EdgeInsets.only(top: 24), sliver: SliverToBoxAdapter( @@ -48,142 +56,157 @@ class HistorySection extends StatelessWidget { ), ), ) - : SliverList( - delegate: SliverChildBuilderDelegate( - childCount: dashboardViewModel.items.length, - (context, index) => Observer(builder: (_) { - final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1]; - final topPadding = index == 0 ? 0.0 : 18.0; - final item = dashboardViewModel.items[index]; - final nextItem = index == dashboardViewModel.items.length - 1 - ? null - : dashboardViewModel.items[index + 1]; + : SliverPadding( + padding: EdgeInsets.only(bottom: short ? 0 : 144), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, index) => Observer(builder: (_) { + final prevItem = index == 0 ? null : items[index - 1]; + final topPadding = index == 0 ? 0.0 : 18.0; + final item = items[index]; + final nextItem = index == items.length - 1 + ? null + : items[index + 1]; - final roundedBottom = (nextItem == null || nextItem is DateSectionItem); - final roundedTop = (prevItem == null || prevItem is DateSectionItem); + final roundedBottom = (nextItem == null || nextItem is DateSectionItem); + final roundedTop = roundedTopSection && (prevItem == null || prevItem is DateSectionItem); - if (item is TransactionListItem) { - final transaction = item.transaction; - final transactionType = dashboardViewModel.getTransactionType(transaction); + if (item is TransactionListItem) { + final transaction = item.transaction; + final transactionType = dashboardViewModel.getTransactionType(transaction); - if (item.hasTokens && item.assetOfTransaction == null) { - return Container(); - } + if (item.hasTokens && item.assetOfTransaction == null) { + return Container(); + } - CryptoCurrency? asset; - if (transaction.additionalInfo["isLightning"] == true) - asset = CryptoCurrency.btcln; - else - asset = item.assetOfTransaction; + CryptoCurrency? asset; + if (transaction.additionalInfo["isLightning"] == true) + asset = CryptoCurrency.btcln; + else + asset = item.assetOfTransaction; - return GestureDetector( - onTap: () { - final page = getIt.get(param1: transaction); - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => page); - }, - child: HistoryTile( - title: item.formattedTitle + transactionType, - date: DateFormat('HH:mm').format(transaction.date), - amount: item.formattedCryptoAmount, - amountFiat: item.formattedFiatAmount, - hasTokens: item.hasTokens, - chainIconPath: _getChainIconPath(), - roundedBottom: roundedBottom, - roundedTop: roundedTop, - bottomSeparator: !roundedBottom, - direction: item.transaction.direction, - pending: item.transaction.isPending, - asset: asset, - ), - ); - } else if (item is TradeListItem) { - final trade = item.trade; - final tradeFrom = trade.from; - final tradeTo = trade.to; - - return GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.tradeDetails, arguments: trade), - child: HistoryTradeTile( - from: tradeFrom, - to: tradeTo, - provider: trade.provider, - date: - DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()), - amount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.amountFormatted(), - receiveAmount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.receiveAmountFormatted(), - roundedBottom: roundedBottom, - roundedTop: roundedTop, - bottomSeparator: !roundedBottom, - swapState: trade.state, - ), - ); - } else if (item is DateSectionItem) { - return Padding( - padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding), - child: Text(DateFormatter.convertDateTimeToReadableString(item.date), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant))); - } else if (item is OrderListItem) { - return GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.orderDetails, arguments: item.order), - child: HistoryOrderTile( - date: DateFormat('HH:mm').format(item.order.createdAt), - amount: item.orderFormattedAmount, - amountFiat: "", - roundedBottom: roundedBottom, - roundedTop: roundedTop, - bottomSeparator: !roundedBottom, - ), - ); - } else if (item is PayjoinTransactionListItem) { - final session = item.session; - - return GestureDetector( - onTap: () => Navigator.of(context).pushNamed( - Routes.payjoinDetails, - arguments: [item.sessionId, item.transaction], - ), - child: PayjoinHistoryTile( - createdAt: DateFormat('HH:mm').format(session.inProgressSince!), - amount: dashboardViewModel.appStore.amountParsingProxy - .asDisplayString(Money( - session.amount, CryptoCurrency.btc)), - currency: item.transaction?.from ?? "BTC", - state: item.status, - isSending: session.isSenderSession, - roundedTop: roundedTop, + return GestureDetector( + onTap: () { + final page = getIt.get(param1: transaction); + if (detailsAsPage) { + Navigator.of(context).push( + CupertinoPageRoute(builder: (context) => Material(child: page))); + } else { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => + FractionallySizedBox(heightFactor: 0.9, child: page)); + } + }, + child: HistoryTile( + title: item.formattedTitle + transactionType, + date: _formatTransactionDate(item.date, localeName), + amount: item.formattedCryptoAmount, + amountFiat: item.formattedFiatAmount, + hasTokens: item.hasTokens, + chainIconPath: _getChainIconPath(), roundedBottom: roundedBottom, - bottomSeparator: !roundedBottom), - ); - } else if (item is AnonpayTransactionListItem) { - final transactionInfo = item.transaction; + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + direction: item.transaction.direction, + pending: item.transaction.isPending, + asset: asset, + ), + ); + } else if (item is TradeListItem) { + final trade = item.trade; + final tradeFrom = trade.from; + final tradeTo = trade.to; - return GestureDetector( + return GestureDetector( onTap: () => Navigator.of(context) - .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo), - child: AnonpayHistoryTile( - provider: transactionInfo.provider, - createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), - amount: transactionInfo.fiatAmount?.toString() ?? - (transactionInfo.amountTo?.toString() ?? ''), - currency: transactionInfo.fiatAmount != null - ? transactionInfo.fiatEquiv ?? '' - : CryptoCurrency.fromFullName(transactionInfo.coinTo) - .name - .toUpperCase(), + .pushNamed(Routes.tradeDetails, arguments: trade), + child: HistoryTradeTile( + from: tradeFrom, + to: tradeTo, + provider: trade.provider, + date: _formatTransactionDate(item.trade.createdAt ?? DateTime.now(), localeName), + amount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.amountFormatted(), + receiveAmount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.receiveAmountFormatted(), + roundedBottom: roundedBottom, + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + swapState: trade.state, + ), + ); + } else if (item is SpecificDateSectionItem) { + return Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding), + child: Text(item.text, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant))); + } else if (item is DateSectionItem) { + return Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding), + child: Text(DateFormat("MMMM yyyy", localeName).format(item.date), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant))); + } else if (item is OrderListItem) { + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.orderDetails, arguments: item.order), + child: HistoryOrderTile( + date: _formatTransactionDate(item.order.createdAt, localeName), + amount: item.orderFormattedAmount, + amountFiat: "", + roundedBottom: roundedBottom, + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + ), + ); + } else if (item is PayjoinTransactionListItem) { + final session = item.session; + + return GestureDetector( + onTap: () => Navigator.of(context).pushNamed( + Routes.payjoinDetails, + arguments: [item.sessionId, item.transaction], + ), + child: PayjoinHistoryTile( + createdAt: _formatTransactionDate(session.inProgressSince!, localeName), + amount: dashboardViewModel.appStore.amountParsingProxy + .asDisplayString(Money( + session.amount, CryptoCurrency.btc)), + currency: item.transaction?.from ?? "BTC", + state: item.status, + isSending: session.isSenderSession, roundedTop: roundedTop, roundedBottom: roundedBottom, - bottomSeparator: !roundedBottom)); - } else - return Text(item.runtimeType.toString()); - }), + bottomSeparator: !roundedBottom), + ); + } else if (item is AnonpayTransactionListItem) { + final transactionInfo = item.transaction; + + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo), + child: AnonpayHistoryTile( + provider: transactionInfo.provider, + createdAt: _formatTransactionDate(transactionInfo.createdAt, localeName), + amount: transactionInfo.fiatAmount?.toString() ?? + (transactionInfo.amountTo?.toString() ?? ''), + currency: transactionInfo.fiatAmount != null + ? transactionInfo.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(transactionInfo.coinTo) + .name + .toUpperCase(), + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: !roundedBottom)); + } else + return Text(item.runtimeType.toString()); + }), + ), ), - ), + ); + }, )); } @@ -196,4 +219,36 @@ class HistorySection extends StatelessWidget { return dashboardViewModel.wallet.currency.chainIconPath ?? ""; } } + + String _formatTransactionDate(DateTime date, String localeName) { + final time = DateFormat.Hm(localeName); + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final thatDay = DateTime(date.year, date.month, date.day); + final daysAgo = today.difference(thatDay).inDays; + + final timeStr = time.format(date); + + if (daysAgo == 0) { + return timeStr; + } + + if (daysAgo == 1) { + return "${S.current.yesterday}, $timeStr"; + } + + if (daysAgo < 7) { + final weekday = DateFormat.EEEE(localeName).format(date); + return "$weekday, $timeStr"; + } + + if (date.year == now.year) { + final dayMonth = DateFormat("d MMMM", localeName).format(date); + return "$dayMonth, $timeStr"; + } + + final full = DateFormat("d MMM yyyy", localeName).format(date); + return "$full, $timeStr"; + } } diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart new file mode 100644 index 000000000..46de3fb36 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/select_deselect_all.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class HistorySwapProvidersPage extends StatelessWidget { + const HistorySwapProvidersPage({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + ModalTopBar( + title: S.of(context).filter_swap_providers, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Observer( + builder: (_) => Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Material( + color: Colors.transparent, + child: SelectDeselectAllBar( + title: S.of(context).swap_providers, + onSelected: (val) { + if ((val && + dashboardViewModel.tradeFilterStore.enabledProvidersCount == 0) || + (!val && + dashboardViewModel.tradeFilterStore.enabledProvidersCount > 0)) { + dashboardViewModel.tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.all); + } + }), + ), + ), + NewListSections(sections: { + "": dashboardViewModel.exchangeFilterItems + .whereType() + .map((item) => ListItemCheckbox( + iconPath: item.providerDescription.image, + keyValue: item.caption, + label: item.caption, + value: item.value(), + onChanged: (val) => dashboardViewModel.tradeFilterStore + .toggleDisplayExchange(item.providerDescription))) + .toList() + }), + ], + ), + ), + ), + )) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart index 44fe3882f..df8e51cd3 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart @@ -87,7 +87,7 @@ class HistoryTileBase extends StatelessWidget { bottomLeft: Radius.circular(roundedBottom ? 22 : 0), bottomRight: Radius.circular(roundedBottom ? 22 : 0), )), - color: Theme.of(context).colorScheme.onInverseSurface, + color: Theme.of(context).colorScheme.surfaceContainer, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -151,7 +151,7 @@ class HistoryTileBase extends StatelessWidget { ), ), if(bottomSeparator) Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: Theme.of(context).colorScheme.surfaceContainer, child: Padding( padding: EdgeInsets.only(left: 56, right: 16), child: Container(height: 1, color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175)), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart new file mode 100644 index 000000000..a46bb1ced --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart @@ -0,0 +1,61 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:flutter/material.dart'; + +class HistoryTopBar extends StatelessWidget { + const HistoryTopBar({super.key, required this.onTap, required this.roundedBottom}); + + final VoidCallback onTap; + final bool roundedBottom; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 24), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(18), + bottom: roundedBottom ? Radius.circular(18) : Radius.zero + ), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + child: Column( + spacing: 12, + children: [ + SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).history), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_right.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ) + ], + ), + if(!roundedBottom) + Container( + height: 1, + width: double.infinity, + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175), + ) + else Container(height: 2) + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart index b01f4dea0..d7c2864cf 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class TransactionDetailsModal extends StatefulWidget { const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false}); @@ -49,163 +50,156 @@ class _TransactionDetailsModalState extends State { Widget build(BuildContext context) { final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount; - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.9, - minChildSize: 0.25, - maxChildSize: 0.9, - snap: true, - snapSizes: const [0.9], - builder: (context, controller) => SafeArea( - bottom: false, - child: Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: GestureDetector( - onTap: FocusScope.of(context).unfocus, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(25))), + return SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GestureDetector( + onTap: FocusScope.of(context).unfocus, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(25))), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).transaction, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), child: Column( children: [ - ModalTopBar( - title: S.of(context).transaction, - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context).pop, + TokenImageWidget( + imageUrl: widget + .transactionDetailsViewModel.transactionAsset.iconPath ?? + "", + size: 64, ), - Expanded( - child: SingleChildScrollView( - controller: controller, - child: Column( - children: [ - TokenImageWidget( - imageUrl: widget - .transactionDetailsViewModel.transactionAsset.iconPath ?? - "", - size: 64, - ), - SizedBox(height: 10), - Text( - widget.transactionDetailsViewModel.formattedTitle + - widget.transactionDetailsViewModel.formattedStatus, - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - ), - CopyWrapper( - requireLongPress: true, - data: ClipboardData(text: transactionInfoAmount.toString()), - builder: (context, copied)=> AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: Text( - key: ValueKey(copied), - copied ? S.of(context).copied : transactionInfoAmount.toStringWithSymbol(), - style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), + SizedBox(height: 10), + Text( + widget.transactionDetailsViewModel.formattedTitle + + widget.transactionDetailsViewModel.formattedStatus, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + CopyWrapper( + requireLongPress: true, + data: ClipboardData(text: transactionInfoAmount.toString()), + builder: (context, copied)=> AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: Text( + key: ValueKey(copied), + copied ? S.of(context).copied : transactionInfoAmount.toStringWithSymbol(), + style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 12, + children: [ + NewListSections(sections: { + "": widget.transactionDetailsViewModel.items + .map((item) { + if (item.value.isEmpty) return null; + + final shouldBuildBottomWidget = + item.value.length > 25; + + return ListItemRegularRow( + copyableText: item.value, + showArrow: false, + keyValue: + ((item.key as ValueKey?)?.value as String?) ?? + item.title, + label: item.title, + trailingWidget: shouldBuildBottomWidget + ? null + : _buildTrailingWIdget(item), + bottomWidget: shouldBuildBottomWidget + ? _buildBottomWidget(item) + : null); + }) + .whereType() + .toList(), + }), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.all(12.0), child: Column( - spacing: 12, + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - NewListSections(sections: { - "": widget.transactionDetailsViewModel.items - .map((item) { - if (item.value.isEmpty) return null; - - final shouldBuildBottomWidget = - item.value.length > 25; - - return ListItemRegularRow( - copyableText: item.value, - showArrow: false, - keyValue: - ((item.key as ValueKey?)?.value as String?) ?? - item.title, - label: item.title, - trailingWidget: shouldBuildBottomWidget - ? null - : _buildTrailingWidget(item), - bottomWidget: shouldBuildBottomWidget - ? _buildBottomWidget(item) - : null); - }) - .whereType() - .toList(), - }), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(S.of(context).note), - TextField( - focusNode: noteFocusNode, - controller: noteController, - decoration: InputDecoration( - hintText: S.of(context).add_a_note, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - isDense: true), - ) - ], - ), - ), - ), - Observer( - builder: (_) => NewListSections(sections: { - "view tx": [ - ListItemRegularRow( - keyValue: "view tx on", - label: widget.transactionDetailsViewModel - .explorerDescription, - onTap: widget - .transactionDetailsViewModel.launchExplorer, - foregroundColor: - Theme.of(context).colorScheme.primary, - trailingIconPath: "assets/new-ui/link_arrow.svg", - trailingIconSize: 8) - ], - if (widget.transactionDetailsViewModel.canReplaceByFee) - "rbf": [ - ListItemRegularRow( - keyValue: "replace by fee", - label: S.of(context).bump_fee, - onTap: () { - Navigator.of(context) - .pushNamed(Routes.bumpFeePage, arguments: [ - widget.transactionDetailsViewModel - .transactionInfo, - widget.transactionDetailsViewModel - .rawTransaction - ]); - }) - ] - }), + Text(S.of(context).note), + TextField( + focusNode: noteFocusNode, + controller: noteController, + decoration: InputDecoration( + hintText: S.of(context).add_a_note, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true), ) ], ), ), - SizedBox(height: MediaQuery.of(context).viewPadding.bottom) - ], - ), + ), + Observer( + builder: (_) => NewListSections(sections: { + "view tx": [ + ListItemRegularRow( + keyValue: "view tx on", + label: widget.transactionDetailsViewModel + .explorerDescription, + onTap: widget + .transactionDetailsViewModel.launchExplorer, + foregroundColor: + Theme.of(context).colorScheme.primary, + trailingIconPath: "assets/new-ui/link_arrow.svg", + trailingIconSize: 8) + ], + if (widget.transactionDetailsViewModel.canReplaceByFee) + "rbf": [ + ListItemRegularRow( + keyValue: "replace by fee", + label: S.of(context).bump_fee, + onTap: () { + Navigator.of(context) + .pushNamed(Routes.bumpFeePage, arguments: [ + widget.transactionDetailsViewModel + .transactionInfo, + widget.transactionDetailsViewModel + .rawTransaction + ]); + }) + ] + }), + ) + ], ), - ) + ), + SizedBox(height: MediaQuery.of(context).viewPadding.bottom) ], ), ), - ), - ), - )); + ) + ], + ), + ), + ), + ), + ); } - Widget _buildTrailingWidget(TransactionDetailsListItem item) { + Widget _buildTrailingWIdget(TransactionDetailsListItem item) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: switch (item.runtimeType) { diff --git a/lib/new-ui/widgets/line_tab_switcher.dart b/lib/new-ui/widgets/line_tab_switcher.dart index b20cd23f7..15927aff6 100644 --- a/lib/new-ui/widgets/line_tab_switcher.dart +++ b/lib/new-ui/widgets/line_tab_switcher.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -29,7 +30,7 @@ class _LineTabSwitcherState extends State { void didUpdateWidget(covariant LineTabSwitcher oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.tabs.length != widget.tabs.length) { + if (!listEquals(oldWidget.tabs, widget.tabs)) { textWidgetKeys = List.generate(widget.tabs.length, (_) => GlobalKey()); textWidgetSizes = []; totalWidth = 0; diff --git a/lib/new-ui/widgets/select_deselect_all.dart b/lib/new-ui/widgets/select_deselect_all.dart new file mode 100644 index 000000000..ca6711ed5 --- /dev/null +++ b/lib/new-ui/widgets/select_deselect_all.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; + +class SelectDeselectAllBar extends StatelessWidget { + const SelectDeselectAllBar({super.key, required this.title, required this.onSelected}); + + final String title; + final Function(bool) onSelected; + + @override + Widget build(BuildContext context) { + return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Row( + spacing: 20, + children: [ + GestureDetector( + onTap: () => onSelected(true), + child: Text( + S.of(context).select_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + GestureDetector( + onTap: () => onSelected(false), + child: Text(S.of(context).unselect_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary)), + ) + ], + ) + ]); + } +} diff --git a/lib/new-ui/widgets/send_page/send_address_input.dart b/lib/new-ui/widgets/send_page/send_address_input.dart index d02b71052..26ae69865 100644 --- a/lib/new-ui/widgets/send_page/send_address_input.dart +++ b/lib/new-ui/widgets/send_page/send_address_input.dart @@ -88,6 +88,8 @@ class _NewSendAddressInputState extends State { children: [ TextField( focusNode: widget.focusNode, + autocorrect: false, + enableSuggestions: false, onSubmitted: (val) => FocusScope.of(context).unfocus(), onChanged: state.didChange, onEditingComplete: () { diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart index a9576d131..2ed9b9398 100644 --- a/lib/new-ui/widgets/send_page/send_amount_input.dart +++ b/lib/new-ui/widgets/send_page/send_amount_input.dart @@ -71,6 +71,8 @@ class _NewSendAmountInputState extends State { signed: false, decimal: widget.maxDecimals > 0, ), + autocorrect: false, + enableSuggestions: false, inputFormatters: [ DecimalInputFormatter(maxDecimals: widget.maxDecimals), ], @@ -87,7 +89,11 @@ class _NewSendAmountInputState extends State { onPressed: () async { final data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - widget.amountController.text = data.text!; + final text = data.text!; + widget.amountController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); } }), ], diff --git a/lib/router.dart b/lib/router.dart index 638329ae8..bd1d5ad0f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -679,7 +679,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.tradeDetails: - return MaterialPageRoute( + return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get(param1: settings.arguments as Trade)); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 3e3bda042..6aeb36a25 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -149,8 +149,8 @@ class ConnectDevicePageBodyState extends State { bleDevices.add(device); if (longWait) longWait = false; })) - ..onError((e) { - throw e.toString(); + ..onError((Object e) { + printV(e); }); _bleRefreshTimer?.cancel(); _bleRefreshTimer = null; diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index 5b89c0e8f..c8d7c86c7 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -62,11 +62,6 @@ class HeaderRow extends StatelessWidget { child: GestureDetector( key: ValueKey('transactions_page_header_row_transaction_filter_button_key'), onTap: () { - showPopUp( - context: context, - builder: (context) => - FilterWidget(filterItems: dashboardViewModel.filterItems), - ); }, child: Semantics( label: 'Transaction Filter', diff --git a/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart b/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart index 823328723..a2a75d552 100644 --- a/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart +++ b/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart @@ -11,6 +11,7 @@ class ListItemCheckboxWidget extends StatefulWidget { required this.label, required this.value, required this.onChanged, + this.subtitleColor, this.onTap, this.isFirstInSection = false, this.isLastInSection = false, this.subtitle, this.iconPath, this.showArrow = false, @@ -22,6 +23,7 @@ class ListItemCheckboxWidget extends StatefulWidget { final String? iconPath; final bool showArrow; final bool value; + final Color? subtitleColor; final VoidCallback? onTap; final ValueChanged onChanged; final bool isFirstInSection; @@ -81,7 +83,7 @@ class _ListItemCheckboxWidgetState extends State { Text( widget.subtitle!, style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + fontSize: 12, color: widget.subtitleColor ?? Theme.of(context).colorScheme.onSurfaceVariant), ) ], ), diff --git a/lib/src/widgets/new_list_row/new_list_section.dart b/lib/src/widgets/new_list_row/new_list_section.dart index 8abef379d..ad78013c8 100644 --- a/lib/src/widgets/new_list_row/new_list_section.dart +++ b/lib/src/widgets/new_list_row/new_list_section.dart @@ -133,6 +133,7 @@ class NewListSections extends StatelessWidget { label: item.label, subtitle: item.subtitle, iconPath: item.iconPath, + subtitleColor: item.subtitleColor, showArrow: item.showArrow, onTap: item.onTap, value: item.value, diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 345a37b6c..57e3149ee 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -69,6 +69,23 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayNearIntents; + @computed + int get enabledProvidersCount => [ + displayChangeNow, + displaySideShift, + displaySimpleSwap, + displayTrocador, + displayExolix, + displayChainflip, + displayThorChain, + displayLetsExchange, + displayStealthEx, + displayXOSwap, + displaySwapTrade, + displaySwapXyz, + displayNearIntents + ].where((item) => item).length; + @computed bool get displayAllTrades => displayChangeNow && diff --git a/lib/utils/decimal_input_formatter.dart b/lib/utils/decimal_input_formatter.dart index 18a2b5586..b45a554e8 100644 --- a/lib/utils/decimal_input_formatter.dart +++ b/lib/utils/decimal_input_formatter.dart @@ -11,7 +11,12 @@ class DecimalInputFormatter extends TextInputFormatter { final text = newValue.text; if (text.isEmpty) return newValue; - if (S.current.all.startsWith(text)) return TextEditingValue(text: ""); + if (S.current.all.startsWith(text)) { + return const TextEditingValue( + text: "", + selection: TextSelection.collapsed(offset: 0), + ); + } final regex = maxDecimals == 0 ? RegExp(r'^\d*$') : RegExp('^\\d*([.,]\\d{0,$maxDecimals})?\$'); return regex.hasMatch(text) ? newValue.copyWith(text: text.replaceAll(',', '.')) : oldValue; diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index eee768c61..abbef80cb 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -277,6 +277,7 @@ class ExceptionHandler { "invalid signature", "invalid password", "NetworkImage._loadAsync", + "Invalid image data", "SSLV3_ALERT_BAD_RECORD_MAC", "PlatformException(already_active, File picker is already active", // SVG-related errors @@ -296,6 +297,10 @@ class ExceptionHandler { "Wrong Device Status: 0x5515 (UNKNOWN)", "Command handling failed. With error: hostUnreachable", + // Android IME/Gboard occasionally reports a caret offset past the end of + // the text on the platform text-input channel while typing. Non-fatal + // framework<->platform desync; the app keeps working. + "invalid selection start", "FocusScopeNode was used after being disposed", "_getDismissibleFlushbar", "_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)", @@ -456,6 +461,19 @@ class ExceptionHandler { } } + if (errorDetails.exception.toString().contains("Cannot add event after closing")) { + // grpc-dart teardown race (e.g. the MWEB channel on litecoin): a buffered outgoing + // frame is delivered to the http2 stream's sink after the call/channel was + // terminated. The message alone is too generic to ignore, so require the exact + // shape of grpc's forwarding chain: .map().map().handleError().listen(sink.add). + final stack = errorDetails.stack.toString(); + if (stack.contains("_StreamSinkWrapper.add") && + stack.contains("_MapStream._handleData") && + stack.contains("_HandleErrorStream._handleData")) { + return true; + } + } + return false; } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index f9d355c5a..77c59b762 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart'; import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; import "package:cw_core/balance_card_style_settings.dart"; import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; @@ -95,111 +96,8 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, - filterItems = { - S.current.transactions: [ - FilterItem( - value: () => transactionFilterStore.displayAll, - caption: S.current.all_transactions, - onChanged: transactionFilterStore.toggleAll), - FilterItem( - value: () => transactionFilterStore.displayIncoming, - caption: S.current.incoming, - onChanged: transactionFilterStore.toggleIncoming), - FilterItem( - value: () => transactionFilterStore.displayOutgoing, - caption: S.current.outgoing, - onChanged: transactionFilterStore.toggleOutgoing), - if (appStore.wallet!.type == WalletType.bitcoin) - FilterItem( - value: () => transactionFilterStore.displaySilentPayments, - caption: S.current.silent_payments, - onChanged: transactionFilterStore.toggleSilentPayments, - ), - // FilterItem( - // value: () => false, - // caption: S.current.transactions_by_date, - // onChanged: null), - ], - 'Orders': [ - FilterItem( - value: () => orderFilterStore.displayCakePay, - caption: 'Cake Pay', - onChanged: () => - orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)), - ], - S.current.trades: [ - FilterItem( - value: () => tradeFilterStore.displayAllTrades, - caption: S.current.all_trades, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)), - FilterItem( - value: () => tradeFilterStore.displayChangeNow, - caption: ExchangeProviderDescription.changeNow.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)), - FilterItem( - value: () => tradeFilterStore.displaySideShift, - caption: ExchangeProviderDescription.sideShift.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)), - FilterItem( - value: () => tradeFilterStore.displaySimpleSwap, - caption: ExchangeProviderDescription.simpleSwap.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)), - FilterItem( - value: () => tradeFilterStore.displayTrocador, - caption: ExchangeProviderDescription.trocador.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)), - FilterItem( - value: () => tradeFilterStore.displayExolix, - caption: ExchangeProviderDescription.exolix.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), - FilterItem( - value: () => tradeFilterStore.displayChainflip, - caption: ExchangeProviderDescription.chainflip.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)), - FilterItem( - value: () => tradeFilterStore.displayThorChain, - caption: ExchangeProviderDescription.thorChain.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), - FilterItem( - value: () => tradeFilterStore.displayLetsExchange, - caption: ExchangeProviderDescription.letsExchange.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), - FilterItem( - value: () => tradeFilterStore.displayStealthEx, - caption: ExchangeProviderDescription.stealthEx.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), - FilterItem( - value: () => tradeFilterStore.displayXOSwap, - caption: ExchangeProviderDescription.xoSwap.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)), - FilterItem( - value: () => tradeFilterStore.displaySwapTrade, - caption: ExchangeProviderDescription.swapTrade.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)), - FilterItem( - value: () => tradeFilterStore.displaySwapXyz, - caption: ExchangeProviderDescription.swapsXyz.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)), - FilterItem( - value: () => tradeFilterStore.displayNearIntents, - caption: ExchangeProviderDescription.nearIntents.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.nearIntents)), - ] - }, + filterItems = [], + exchangeFilterItems = [], subname = '', name = appStore.wallet!.name, type = appStore.wallet!.type, @@ -220,6 +118,8 @@ abstract class DashboardViewModelBase with Store { unawaited(_loadConstraints()); final _wallet = wallet; + loadFilterItems(); + if (_wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(_wallet).label; @@ -352,6 +252,110 @@ abstract class DashboardViewModelBase with Store { tradeMonitor.monitorActiveTrades(wallet.id); } + + void loadFilterItems() { + filterItems = [ + // FilterItem( + // value: () => transactionFilterStore.displayAll, + // caption: S.current.all_transactions, + // onChanged: transactionFilterStore.toggleAll), + FilterItem( + value: () => transactionFilterStore.displayOutgoing, + caption: S.current.send, + onChanged: transactionFilterStore.toggleOutgoing), + FilterItem( + value: () => transactionFilterStore.displayIncoming, + caption: S.current.receive, + onChanged: transactionFilterStore.toggleIncoming), + if (appStore.wallet!.type == WalletType.bitcoin) + FilterItem( + value: () => transactionFilterStore.displaySilentPayments, + caption: S.current.silent_payments, + onChanged: transactionFilterStore.toggleSilentPayments, + ), + SwapFilterItem( + enabledProviders: () => tradeFilterStore.enabledProvidersCount, + allEnabled: () => tradeFilterStore.displayAllTrades, + value: () => tradeFilterStore.enabledProvidersCount>0, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)), + FilterItem( + value: () => orderFilterStore.displayCakePay, + caption: 'Cake Pay', + onChanged: () => + orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)), + ]; + exchangeFilterItems = [ + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.changeNow, + value: () => tradeFilterStore.displayChangeNow, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.sideShift, + value: () => tradeFilterStore.displaySideShift, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.simpleSwap, + value: () => tradeFilterStore.displaySimpleSwap, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.trocador, + value: () => tradeFilterStore.displayTrocador, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.exolix, + value: () => tradeFilterStore.displayExolix, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.chainflip, + value: () => tradeFilterStore.displayChainflip, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.thorChain, + value: () => tradeFilterStore.displayThorChain, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.letsExchange, + value: () => tradeFilterStore.displayLetsExchange, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.stealthEx, + value: () => tradeFilterStore.displayStealthEx, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.xoSwap, + value: () => tradeFilterStore.displayXOSwap, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.swapTrade, + value: () => tradeFilterStore.displaySwapTrade, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.swapsXyz, + value: () => tradeFilterStore.displaySwapXyz, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)), + SwapProviderFilterItem( + providerDescription: ExchangeProviderDescription.nearIntents, + value: () => tradeFilterStore.displayNearIntents, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)), + ]; + } + + + bool _isTransactionDisposerCallbackRunning = false; @action @@ -405,6 +409,20 @@ abstract class DashboardViewModelBase with Store { return false; } + @action + void changeAllFilterItems(bool value) { + for (final item in filterItems) { + if (item.value() != value) { + item.onChanged(); + } + } + for (final item in exchangeFilterItems) { + if (item.value() != value) { + item.onChanged(); + } + } + } + @action Future loadCardDesigns() async { final accountStyleSettings = @@ -715,6 +733,14 @@ abstract class DashboardViewModelBase with Store { return formattedItemsList(_items); } + static const shortHistoryLength = 3; + + @computed + List get itemsShort => items + .where((item) => item is! DateSectionItem) + .take(shortHistoryLength) + .toList(); + @observable WalletBase, TransactionInfo> wallet; @@ -1123,7 +1149,11 @@ abstract class DashboardViewModelBase with Store { PayjoinTransactionsStore payjoinTransactionsStore; - Map> filterItems; + // Map> filterItems; + + List filterItems; + + List exchangeFilterItems; bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; @@ -1234,6 +1264,7 @@ abstract class DashboardViewModelBase with Store { this.wallet = wallet; type = wallet.type; name = wallet.name; + loadFilterItems(); if (wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(wallet).label; diff --git a/lib/view_model/dashboard/date_section_item.dart b/lib/view_model/dashboard/date_section_item.dart index 75250a7ea..5bc607dcf 100644 --- a/lib/view_model/dashboard/date_section_item.dart +++ b/lib/view_model/dashboard/date_section_item.dart @@ -1,8 +1,36 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class DateSectionItem extends ActionListItem { DateSectionItem(this.date, {required super.key}); - + @override final DateTime date; -} \ No newline at end of file +} + +abstract class SpecificDateSectionItem extends DateSectionItem { + SpecificDateSectionItem(super.date, {required super.key}); + + String get text; +} + +class Last30daysTransactionItem extends SpecificDateSectionItem { + Last30daysTransactionItem(super.date, {required super.key}); + + @override + String get text => S.current.last_30_days; +} + +class Last7daysTransactionItem extends SpecificDateSectionItem { + Last7daysTransactionItem(super.date, {required super.key}); + + @override + String get text => S.current.last_7_days; +} + +class TodayTransactionItem extends SpecificDateSectionItem { + TodayTransactionItem(super.date, {required super.key}); + + @override + String get text => S.current.today; +} diff --git a/lib/view_model/dashboard/filter_item.dart b/lib/view_model/dashboard/filter_item.dart index 0944e899c..ffd0a35a3 100644 --- a/lib/view_model/dashboard/filter_item.dart +++ b/lib/view_model/dashboard/filter_item.dart @@ -1,4 +1,4 @@ -import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; class FilterItem { FilterItem({ @@ -9,4 +9,32 @@ class FilterItem { bool Function() value; String caption; Function onChanged; +} + +class SwapFilterItem extends FilterItem { + SwapFilterItem({ + required this.enabledProviders, + required this.allEnabled, + required super.value, + super.caption = "Swap", + required super.onChanged, + }); + + int Function() enabledProviders; + bool Function() allEnabled; +} + + +class SwapProviderFilterItem extends FilterItem { + SwapProviderFilterItem({ + required super.value, + required super.onChanged, + required this.providerDescription, + super.caption = "" +}); + + @override + String get caption => providerDescription.title; + + final ExchangeProviderDescription providerDescription; } \ No newline at end of file diff --git a/lib/view_model/dashboard/formatted_item_list.dart b/lib/view_model/dashboard/formatted_item_list.dart index 04464ca89..45dfb663d 100644 --- a/lib/view_model/dashboard/formatted_item_list.dart +++ b/lib/view_model/dashboard/formatted_item_list.dart @@ -1,45 +1,82 @@ -import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; -import 'package:flutter/foundation.dart'; +import "package:cake_wallet/view_model/dashboard/action_list_item.dart"; +import "package:cake_wallet/view_model/dashboard/date_section_item.dart"; +import "package:flutter/foundation.dart"; + +enum _DateBucket { recent, last7Days, last30Days, byMonth } List formattedItemsList(List items) { final formattedList = []; - DateTime? lastDate; items.sort((a, b) => b.date.compareTo(a.date)); - for (var i = 0; i < items.length; i++) { - final transaction = items[i]; + final now = DateTime.now(); + final todayTreshold = DateTime(now.year, now.month, now.day); + final last7daysThreshold = now.subtract(const Duration(days: 7)); + final last30daysThreshold = now.subtract(const Duration(days: 30)); - if (lastDate == null) { - lastDate = transaction.date; - formattedList.add( - DateSectionItem( - transaction.date, - key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), - ), - ); - formattedList.add(transaction); - continue; + _DateBucket? lastBucket; + DateTime? lastMonthDate; + + for (final transaction in items) { + final date = transaction.date; + + final bucket = date.isAfter(todayTreshold) + ? _DateBucket.recent + : date.isAfter(last7daysThreshold) + ? _DateBucket.last7Days + : date.isAfter(last30daysThreshold) + ? _DateBucket.last30Days + : _DateBucket.byMonth; + + switch (bucket) { + case _DateBucket.recent: + if (lastBucket != _DateBucket.recent) { + formattedList.add( + TodayTransactionItem( + date, + key: const ValueKey("today_section_item_key"), + ), + ); + } + break; + case _DateBucket.last7Days: + if (lastBucket != _DateBucket.last7Days) { + formattedList.add( + Last7daysTransactionItem( + date, + key: const ValueKey("last_7_days_section_item_key"), + ), + ); + } + break; + case _DateBucket.last30Days: + if (lastBucket != _DateBucket.last30Days) { + formattedList.add( + Last30daysTransactionItem( + date, + key: const ValueKey("last_30_days_section_item_key"), + ), + ); + } + break; + case _DateBucket.byMonth: + final isNewMonth = lastMonthDate == null || + lastMonthDate.year != date.year || + lastMonthDate.month != date.month; + if (isNewMonth) { + lastMonthDate = date; + formattedList.add( + DateSectionItem( + date, + key: ValueKey("date_section_item_${date.year}_${date.month}_key"), + ), + ); + } + break; } - final isCurrentDay = lastDate.year == transaction.date.year && - lastDate.month == transaction.date.month && - lastDate.day == transaction.date.day; - - if (isCurrentDay) { - formattedList.add(transaction); - continue; - } - - lastDate = transaction.date; - formattedList.add( - DateSectionItem( - transaction.date, - key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), - ), - ); + lastBucket = bucket; formattedList.add(transaction); } return formattedList; -} \ No newline at end of file +} diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 405246319..5a3b003b7 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -51,7 +51,7 @@ class TransactionListItem extends ActionListItem with Keyable { .withLocalSeperator(_appStore.settingsStore.languageCode); } - return transaction.amount.toStringWithSymbol(); + return transaction.amount.toStringWithSymbol(fractionalDigits: 8); } String get formattedTitle { diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 0cc1bdb82..96861a10e 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -29,6 +29,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -75,8 +76,7 @@ class TxDetailRowDefinition { TxDetailRowDefinition( keyString: "standard_list_item_transaction_details_date_key", title: S.current.transaction_details_date, - valueGetter: (vm) => DateFormatter.withCurrentLocal().format(vm.transactionInfo.date)), - + valueGetter: (vm) => DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode).format(vm.transactionInfo.date)), TxDetailRowDefinition( keyString: "standard_list_item_transaction_details_height_key", title: S.current.transaction_details_height, diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1523c53e2..efde482cf 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -55,6 +55,7 @@ "alert_notice": "Notice", "all": "ALL", "all_coins": "All Coins", + "all_pascal_case": "All", "all_trades": "All trades", "all_transactions": "All transactions", "alphabetical": "Alphabetical", @@ -489,6 +490,7 @@ "file_saved": "File saved", "fill_code": "Please fill in the verification code provided to your email", "filter_by": "Filter by", + "filter_swap_providers": "Filter Swap Providers", "filters": "Filters", "finding_provider": "Finding provider", "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven", @@ -570,6 +572,7 @@ "label": "Label", "label_address": "Label address", "last_30_days": "Last 30 days", + "last_7_days": "Last 7 days", "learn_more": "Learn More", "ledger_connection_error": "Failed to connect to your Ledger. Please try again.", "ledger_error_device_locked": "The Ledger is locked", @@ -684,6 +687,7 @@ "no_id_needed": "No ID needed!", "no_id_required": "No ID required. Top up and spend anywhere", "no_providers_available": "No providers available", + "no_providers_selected": "No Providers Selected", "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use.", "no_relays": "No relays", "no_relays_message": "We found a Nostr NIP-05 record for this user, but it does not contain any relays. Please instruct the recipient to add relays to their Nostr record.", @@ -873,6 +877,7 @@ "rescan": "Rescan", "resend_code": "Please resend it", "reset": "Reset", + "reset_filters": "Reset Filters", "reset_password": "Reset Password", "restore": "Restore", "restore_active_seed": "Active seed", @@ -1288,6 +1293,7 @@ "tx_wrong_balance_exception": "You do not have enough ${currency} to send this amount.", "tx_wrong_balance_with_amount_exception": "You do not have enough ${currency} to send the total amount of ${amount}", "tx_zero_fee_exception": "Cannot send transaction with 0 fee. Try increasing the rate or checking your connection for latest estimates.", + "type": "Type", "unavailable_balance": "Unavailable balance", "unavailable_balance_description": "Unavailable Balance: This total includes funds that are locked in pending transactions and those you have actively frozen in your Coin Control settings. Locked balances will become available once their respective transactions are completed, while frozen balances remain inaccessible for transactions until you decide to unfreeze them.", "unconfirmed": "Unconfirmed Balance", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3b5560617..91c41c6f2 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -555,6 +555,7 @@ "keys": "Claves", "label": "Etiqueta", "label_address": "Etiquetar dirección", + "last_7_days": "Últimos 7 días", "last_30_days": "Últimos 30 días", "learn_more": "Más información", "ledger_connection_error": "No se pudo conectar a tu Ledger. Inténtalo de nuevo.",