tx history improved layout

Robert Malikowski committed Jun 26, 2026 at 01:40 UTC ac60a6eccea86566e9c32bc0ff44532e87def71d
21 files changed +915 -344
lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart
+2
@@ -6,6 +6,7 @@ class ListItemCheckbox extends ListItem {
6 required super.keyValue,
7 required super.label,
8 this.subtitle,
9 + this.subtitleColor,
10 this.iconPath,
11 this.onTap,
12 this.showArrow = false,
@@ -18,5 +19,6 @@ class ListItemCheckbox extends ListItem {
19 final bool value;
20 final String? subtitle;
21 final String? iconPath;
22 + final Color? subtitleColor;
23 final bool showArrow;
24 }
lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart
+23 -1
@@ -1,11 +1,15 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/new-ui/modal_navigator.dart';
3 import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart';
4 +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_modal.dart';
5 +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_top_bar.dart';
6 import 'package:cake_wallet/reactions/wallet_connect.dart';
7 import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
8 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
9 import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
10 import 'package:flutter/material.dart';
11 import 'package:mobx/mobx.dart';
12 +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
13 import 'assets_section.dart';
14 import 'history_section.dart';
15
@@ -42,6 +46,8 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
46 AssetsHistorySectionTab(
47 S.current.history,
48 HistorySection(
49 + detailsAsPage: false,
50 + roundedTopSection: tabs.length > 1,
51 dashboardViewModel: widget.dashboardViewModel,
52 short: true,
53 )),
@@ -71,8 +77,9 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
77 Widget build(BuildContext context) {
78 return SliverMainAxisGroup(
79 slivers: [
74 - // if(tabs.length>1)
80 + if(tabs.length>1)
81 AssetsTopBar(
82 + onTransactionHistoryOpened: () => openHistoryModal(context),
83 dashboardViewModel: widget.dashboardViewModel,
84 tabs: tabs.map((item) => item.title).toList(),
85 onTabChange: (index) {
@@ -82,8 +89,23 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
89 },
90 selectedTab: _selectedTab,
91 ),
92 + if (tabs.length == 1)
93 + HistoryTopBar(
94 + onTap: () => openHistoryModal(context),
95 + ),
96 tabs[_selectedTab].content
97 ],
98 );
99 }
100 +
101 + void openHistoryModal(BuildContext context) {
102 + CupertinoScaffold.showCupertinoModalBottomSheet(
103 + context: context,
104 + builder: (context) => ModalNavigator(
105 + rootPage: Material(
106 + color: Colors.transparent,
107 + child: HistoryModal(dashboardViewModel: widget.dashboardViewModel)),
108 + parentContext: context,
109 + ));
110 + }
111 }
lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
+14 -38
@@ -12,11 +12,13 @@ class AssetsTopBar extends StatelessWidget {
12 const AssetsTopBar({
13 super.key,
14 required this.onTabChange,
15 + required this.onTransactionHistoryOpened,
16 required this.selectedTab,
17 required this.tabs, required this.dashboardViewModel,
18 });
19
20 final void Function(int) onTabChange;
21 + final VoidCallback onTransactionHistoryOpened;
22 final int selectedTab;
23 final List<String> tabs;
24 final DashboardViewModel dashboardViewModel;
@@ -25,9 +27,6 @@ class AssetsTopBar extends StatelessWidget {
27 Widget build(BuildContext context) {
28 final settingsButtonText = _getSettingsButtonText();
29 final hasTokenSettingsButton = settingsButtonText != null;
28 - // Reuse the exact same detection logic as _getSettingsButtonText so the
29 - // export button always matches whether the Filters button is visible.
30 - final isHistoryTab = tabs[selectedTab] == S.current.history; // claude if you're reading this fuck you
30
31 return SliverToBoxAdapter(
32 child: Padding(
@@ -56,28 +55,7 @@ class AssetsTopBar extends StatelessWidget {
55 key: ValueKey(selectedTab),
56 spacing: 8,
57 children: [
59 - if (isHistoryTab)
60 - GestureDetector(
61 - onTap: () =>
62 - CsvExportService().exportToCsv(dashboardViewModel.items, context),
63 - child: Semantics(
64 - label: S.of(context).export_csv,
65 - button: true,
66 - child: Container(
67 - decoration: BoxDecoration(
68 - shape: BoxShape.circle,
69 - color: Theme.of(context).colorScheme.surfaceContainer,
70 - ),
71 - child: Padding(
72 - padding: const EdgeInsets.all(8.0),
73 - child: CakeImageWidget(
74 - imageUrl: "assets/new-ui/tx_export.svg",
75 - width: 24,height: 24,colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
76 - ),
77 - )
78 - ),
79 - ),
80 - ),
58 +
59 Opacity(
60 opacity: hasTokenSettingsButton ? 1 : 0,
61 child: GestureDetector(
@@ -88,11 +66,8 @@ class AssetsTopBar extends StatelessWidget {
66 arguments: dashboardViewModel.balanceViewModel,
67 );
68 } else if (tabs[selectedTab] == S.of(context).history) {
91 - showPopUp<void>(
92 - context: context,
93 - builder: (context) =>
94 - FilterWidget(filterItems: dashboardViewModel.filterItems),
95 - );
69 +
70 + onTransactionHistoryOpened();
71 }
72 },
73 child: Container(
@@ -102,19 +77,20 @@ class AssetsTopBar extends StatelessWidget {
77 color: Theme.of(context).colorScheme.surfaceContainer,
78 ),
79 child: Padding(
105 - padding: const EdgeInsets.symmetric(horizontal: 8.0),
80 + padding: const EdgeInsets.symmetric(horizontal: 12.0),
81 child: Row(
82 spacing: 6,
83 children: [
109 - CakeImageWidget(
110 - imageUrl: _getSettingsButtonIconPath(),
111 - colorFilter: ColorFilter.mode(
112 - Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
84 +
85 if ((settingsButtonText ?? "").isNotEmpty)
86 Text(
87 settingsButtonText ?? "",
88 style: TextStyle(color: Theme.of(context).colorScheme.primary),
117 - )
89 + ),
90 + CakeImageWidget(
91 + imageUrl: _getSettingsButtonIconPath(),
92 + colorFilter: ColorFilter.mode(
93 + Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
94 ],
95 ),
96 ),
@@ -132,7 +108,7 @@ class AssetsTopBar extends StatelessWidget {
108
109 String? _getSettingsButtonIconPath() {
110 if (tabs[selectedTab] == S.current.history) {
135 - return "assets/new-ui/filter_options.svg";
111 + return "assets/new-ui/arrow_right.svg";
112 }
113
114 if (tabs[selectedTab] == S.current.assets &&
@@ -150,7 +126,7 @@ class AssetsTopBar extends StatelessWidget {
126 }
127
128 if (tabs[selectedTab] == S.current.history) {
153 - return "";
129 + return S.current.all_pascal_case;
130 }
131 return null;
132 }
lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart new
+117
@@ -0,0 +1,117 @@
1 +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
2 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 +import 'package:cake_wallet/generated/i18n.dart';
4 +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart';
5 +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
6 +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
7 +import 'package:cake_wallet/new-ui/widgets/select_deselect_all.dart';
8 +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
9 +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
10 +import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
11 +import 'package:flutter/cupertino.dart';
12 +import 'package:flutter/material.dart';
13 +import 'package:flutter_mobx/flutter_mobx.dart';
14 +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
15 +
16 +class HistoryFiltersPage extends StatelessWidget {
17 + const HistoryFiltersPage({super.key, required this.dashboardViewModel});
18 +
19 + final DashboardViewModel dashboardViewModel;
20 +
21 + @override
22 + Widget build(BuildContext context) {
23 + return Container(
24 + color: Theme.of(context).colorScheme.surface,
25 + child: Column(
26 + children: [
27 + ModalTopBar(
28 + title: S.of(context).filters,
29 + leadingIcon: Icon(Icons.arrow_back_ios_new),
30 + onLeadingPressed: Navigator.of(context).pop,
31 + ),
32 + Expanded(
33 + child: SingleChildScrollView(
34 + controller: ModalScrollController.of(context),
35 + child: Padding(
36 + padding: const EdgeInsets.symmetric(horizontal: 18.0),
37 + child: Observer(
38 + builder: (_) => Column(
39 + children: [
40 + Padding(
41 + padding: const EdgeInsets.only(bottom: 12.0),
42 + child: Material(
43 + color: Colors.transparent,
44 + child: SelectDeselectAllBar(
45 + title: S.of(context).type,
46 + onSelected: dashboardViewModel.changeAllFilterItems),
47 + ),
48 + ),
49 + NewListSections(
50 + sections: {
51 + "": dashboardViewModel.filterItems.map((item) {
52 + if (item is SwapFilterItem) {
53 + final String subtitle;
54 + final Color subtitleColor;
55 + if (item.value()) {
56 + subtitle = S.of(context).manage_providers;
57 + subtitleColor = Theme.of(context).colorScheme.onSurfaceVariant;
58 + } else if (dashboardViewModel.tradeFilterStore.enabledProviders == 0) {
59 + subtitle = S.of(context).no_providers_selected;
60 + subtitleColor = Color(0xFFFFB84E);
61 + } else {
62 + subtitle = "${item.enabledProviders()} ${S.of(context).providers}";
63 + subtitleColor = Theme.of(context).colorScheme.primary;
64 + }
65 +
66 + return ListItemCheckbox(
67 + onTap: () {
68 + Navigator.of(context).push(CupertinoPageRoute(
69 + builder: (context) => HistorySwapProvidersPage(
70 + dashboardViewModel: dashboardViewModel)));
71 + },
72 + keyValue: item.caption,
73 + label: S.of(context).swap,
74 + value: item.value(),
75 + onChanged: (val) {
76 + if ((val &&
77 + dashboardViewModel.tradeFilterStore.enabledProviders ==
78 + 0) ||
79 + (!val &&
80 + dashboardViewModel.tradeFilterStore.enabledProviders >
81 + 0)) {
82 + dashboardViewModel.tradeFilterStore
83 + .toggleDisplayExchange(ExchangeProviderDescription.all);
84 + }
85 + },
86 + subtitle: subtitle,
87 + subtitleColor: subtitleColor,
88 + showArrow: true);
89 + }
90 +
91 + return ListItemCheckbox(
92 + keyValue: item.caption,
93 + label: item.caption,
94 + value: item.value(),
95 + onChanged: (val) => item.onChanged());
96 + }).toList()
97 + },
98 + ),
99 + ],
100 + ),
101 + ),
102 + ),
103 + )),
104 + SafeArea(
105 + child: Padding(
106 + padding: const EdgeInsets.all(18.0),
107 + child: NewPrimaryButton(
108 + onPressed: () => dashboardViewModel.changeAllFilterItems(true),
109 + text: S.of(context).reset_filters,
110 + color: Theme.of(context).colorScheme.surfaceContainer,
111 + textColor: Theme.of(context).colorScheme.primary),
112 + ))
113 + ],
114 + ),
115 + );
116 + }
117 +}
lib/new-ui/widgets/coins_page/assets_history/history_modal.dart new
+119
@@ -0,0 +1,119 @@
1 +import 'package:cake_wallet/core/csv_export_service.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_filters_page.dart';
4 +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_section.dart';
5 +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
7 +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
8 +import 'package:flutter/cupertino.dart';
9 +import 'package:flutter/material.dart';
10 +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
11 +
12 +class HistoryModal extends StatelessWidget {
13 + const HistoryModal({super.key, required this.dashboardViewModel});
14 +
15 + final DashboardViewModel dashboardViewModel;
16 +
17 + @override
18 + Widget build(BuildContext context) {
19 + return Container(
20 + decoration: BoxDecoration(
21 + color: Theme.of(context).colorScheme.surface,
22 + borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
23 + child: Column(
24 + children: [
25 + ModalTopBar(
26 + title: S.of(context).history,
27 + leadingIcon: Icon(Icons.close),
28 + onLeadingPressed: Navigator.of(context).maybePop,
29 + trailingIcon: CakeImageWidget(
30 + imageUrl: "assets/new-ui/tx_export.svg",
31 + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
32 + ),
33 + onTrailingPressed: () =>
34 + CsvExportService().exportToCsv(dashboardViewModel.items, context),
35 + ),
36 + Expanded(
37 + child: Stack(
38 + children: [
39 + Material(
40 + child: CustomScrollView(controller: ModalScrollController.of(context), slivers: [
41 + HistorySection(
42 + detailsAsPage: true,
43 + dashboardViewModel: dashboardViewModel,
44 + short: false,
45 + roundedTopSection: true)
46 + ])),
47 + Positioned(
48 + bottom: 0,
49 + left: 0,
50 + right: 0,
51 + child: Material(
52 + color: Colors.transparent,
53 + child: Container(
54 + height: MediaQuery.of(context).viewPadding.bottom + 168,
55 + decoration: BoxDecoration(
56 + gradient: LinearGradient(
57 + begin: Alignment.bottomCenter,
58 + end: Alignment.topCenter,
59 + colors: <Color>[
60 + Theme.of(context).colorScheme.surface.withAlpha(200),
61 + Theme.of(context).colorScheme.surface.withAlpha(175),
62 + Theme.of(context).colorScheme.surface.withAlpha(150),
63 + Theme.of(context).colorScheme.surface.withAlpha(100),
64 + Theme.of(context).colorScheme.surface.withAlpha(50),
65 + Theme.of(context).colorScheme.surface.withAlpha(25),
66 + Theme.of(context).colorScheme.surface.withAlpha(5),
67 + ],
68 + ),
69 + ),
70 + ),
71 + ),
72 + ),
73 + Positioned(
74 + left: 12,
75 + right: 12,
76 + bottom: MediaQuery.of(context).viewPadding.bottom + 24,
77 + child: Material(
78 + borderRadius: BorderRadius.circular(18),
79 + color: Theme.of(context).colorScheme.surfaceContainer,
80 + child: InkWell(
81 + borderRadius: BorderRadius.circular(18),
82 + onTap: () {
83 + Navigator.of(context).push(CupertinoPageRoute(
84 + builder: (context) =>
85 + HistoryFiltersPage(dashboardViewModel: dashboardViewModel)));
86 + },
87 + child: Material(
88 + color: Colors.transparent,
89 + child: SizedBox(
90 + height: 56,
91 + width: double.infinity,
92 + child: Row(
93 + mainAxisAlignment: MainAxisAlignment.center,
94 + spacing: 10,
95 + children: [
96 + CakeImageWidget(
97 + imageUrl: "assets/new-ui/filter_options.svg",
98 + colorFilter: ColorFilter.mode(
99 + Theme.of(context).colorScheme.primary, BlendMode.srcIn),
100 + ),
101 + Text(
102 + S.of(context).filters,
103 + style: TextStyle(
104 + fontWeight: FontWeight.w500,
105 + color: Theme.of(context).colorScheme.primary),
106 + )
107 + ],
108 + ),
109 + ),
110 + ),
111 + ),
112 + ))
113 + ],
114 + ))
115 + ],
116 + ),
117 + );
118 + }
119 +}
lib/new-ui/widgets/coins_page/assets_history/history_section.dart
+60 -14
@@ -19,22 +19,25 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
19 import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
20 import 'package:cw_core/crypto_currency.dart';
21 import 'package:cw_core/sync_status.dart';
22 +import 'package:flutter/cupertino.dart';
23 import 'package:flutter/material.dart';
24 import 'package:flutter_mobx/flutter_mobx.dart';
25 import 'package:intl/intl.dart';
26
27 class HistorySection extends StatelessWidget {
27 - const HistorySection({super.key, required this.dashboardViewModel, required this.short});
28 + const HistorySection({super.key, required this.dashboardViewModel, required this.short, required this.roundedTopSection, required this.detailsAsPage});
29
30 final DashboardViewModel dashboardViewModel;
31 final bool short;
32 + final bool roundedTopSection;
33 + final bool detailsAsPage;
34
35 @override
36 Widget build(BuildContext context) {
37 final items = short ? dashboardViewModel.itemsShort : dashboardViewModel.items;
38
39 return SliverPadding(
37 - padding: const EdgeInsets.symmetric(horizontal: 16.0),
40 + padding: EdgeInsets.only(left: 16.0, right: 16, top: short && roundedTopSection ? 18 : 0),
41 sliver: Observer(
42 builder: (_) => (items.isEmpty)
43 ? SliverPadding(
@@ -63,7 +66,7 @@ class HistorySection extends StatelessWidget {
66 : items[index + 1];
67
68 final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
66 - final roundedTop = (prevItem == null || prevItem is DateSectionItem);
69 + final roundedTop = roundedTopSection && (prevItem == null || prevItem is DateSectionItem);
70
71 if (item is TransactionListItem) {
72 final transaction = item.transaction;
@@ -82,14 +85,20 @@ class HistorySection extends StatelessWidget {
85 return GestureDetector(
86 onTap: () {
87 final page = getIt.get<TransactionDetailsModal>(param1: transaction);
85 - showModalBottomSheet(
86 - isScrollControlled: true,
87 - context: context,
88 - builder: (context) => page);
88 + if (detailsAsPage) {
89 + Navigator.of(context).push(
90 + CupertinoPageRoute(builder: (context) => Material(child: page)));
91 + } else {
92 + showModalBottomSheet(
93 + isScrollControlled: true,
94 + context: context,
95 + builder: (context) =>
96 + FractionallySizedBox(heightFactor: 0.9, child: page));
97 + }
98 },
99 child: HistoryTile(
100 title: item.formattedTitle + transactionType,
92 - date: DateFormat('HH:mm').format(transaction.date),
101 + date: _formatTransactionDate(item.date),
102 amount: item.formattedCryptoAmount,
103 amountFiat: item.formattedFiatAmount,
104 hasTokens: item.hasTokens,
@@ -117,8 +126,7 @@ class HistorySection extends StatelessWidget {
126 from: tradeFrom,
127 to: tradeTo,
128 provider: trade.provider,
120 - date:
121 - DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()),
129 + date: _formatTransactionDate(item.trade.createdAt ?? DateTime.now()),
130 amount: trade.amountFormatted(),
131 receiveAmount: trade.receiveAmountFormatted(),
132 roundedBottom: roundedBottom,
@@ -127,10 +135,16 @@ class HistorySection extends StatelessWidget {
135 swapState: trade.state,
136 ),
137 );
138 + } else if (item is SpecificDateSectionItem) {
139 + return Padding(
140 + padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
141 + child: Text(item.text,
142 + style: TextStyle(
143 + color: Theme.of(context).colorScheme.onSurfaceVariant)));
144 } else if (item is DateSectionItem) {
145 return Padding(
146 padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
133 - child: Text(DateFormatter.convertDateTimeToReadableString(item.date),
147 + child: Text(DateFormat("MMMM yyyy").format(item.date),
148 style: TextStyle(
149 color: Theme.of(context).colorScheme.onSurfaceVariant)));
150 } else if (item is OrderListItem) {
@@ -138,7 +152,7 @@ class HistorySection extends StatelessWidget {
152 onTap: () => Navigator.of(context)
153 .pushNamed(Routes.orderDetails, arguments: item.order),
154 child: HistoryOrderTile(
141 - date: DateFormat('HH:mm').format(item.order.createdAt),
155 + date: _formatTransactionDate(item.order.createdAt),
156 amount: item.orderFormattedAmount,
157 amountFiat: "USD 0.00",
158 roundedBottom: roundedBottom,
@@ -155,7 +169,7 @@ class HistorySection extends StatelessWidget {
169 arguments: [item.sessionId, item.transaction],
170 ),
171 child: PayjoinHistoryTile(
158 - createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
172 + createdAt: _formatTransactionDate(session.inProgressSince!),
173 amount: dashboardViewModel.appStore.amountParsingProxy
174 .getDisplayCryptoString(
175 session.amount.toInt(), CryptoCurrency.btc),
@@ -174,7 +188,7 @@ class HistorySection extends StatelessWidget {
188 .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo),
189 child: AnonpayHistoryTile(
190 provider: transactionInfo.provider,
177 - createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
191 + createdAt: _formatTransactionDate(transactionInfo.createdAt),
192 amount: transactionInfo.fiatAmount?.toString() ??
193 (transactionInfo.amountTo?.toString() ?? ''),
194 currency: transactionInfo.fiatAmount != null
@@ -202,4 +216,36 @@ class HistorySection extends StatelessWidget {
216 return dashboardViewModel.wallet.currency.chainIconPath ?? "";
217 }
218 }
219 +
220 + String _formatTransactionDate(DateTime date) {
221 + final time = DateFormat.Hm();
222 +
223 + final now = DateTime.now();
224 + final today = DateTime(now.year, now.month, now.day);
225 + final thatDay = DateTime(date.year, date.month, date.day);
226 + final daysAgo = today.difference(thatDay).inDays;
227 +
228 + final timeStr = time.format(date);
229 +
230 + if (daysAgo == 0) {
231 + return timeStr;
232 + }
233 +
234 + if (daysAgo == 1) {
235 + return "${S.current.yesterday}, $timeStr";
236 + }
237 +
238 + if (daysAgo < 7) {
239 + final weekday = DateFormat.EEEE().format(date);
240 + return '$weekday, $timeStr';
241 + }
242 +
243 + if (date.year == now.year) {
244 + final dayMonth = DateFormat("d MMMM").format(date);
245 + return '$dayMonth, $timeStr';
246 + }
247 +
248 + final full = DateFormat("d MMMM yyyy").format(date);
249 + return '$full, $timeStr';
250 + }
251 }
lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart new
+75
@@ -0,0 +1,75 @@
1 +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
2 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 +import 'package:cake_wallet/generated/i18n.dart';
4 +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
5 +import 'package:cake_wallet/new-ui/widgets/select_deselect_all.dart';
6 +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
7 +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
8 +import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
9 +import 'package:flutter/material.dart';
10 +import 'package:flutter_mobx/flutter_mobx.dart';
11 +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
12 +
13 +class HistorySwapProvidersPage extends StatelessWidget {
14 + const HistorySwapProvidersPage({super.key, required this.dashboardViewModel});
15 +
16 + final DashboardViewModel dashboardViewModel;
17 +
18 + @override
19 + Widget build(BuildContext context) {
20 + return Container(
21 + color: Theme.of(context).colorScheme.surface,
22 + child: Column(
23 + children: [
24 + ModalTopBar(
25 + title: S.of(context).filter_swap_providers,
26 + leadingIcon: Icon(Icons.arrow_back_ios_new),
27 + onLeadingPressed: Navigator.of(context).pop,
28 + ),
29 + Expanded(
30 + child: SingleChildScrollView(
31 + controller: ModalScrollController.of(context),
32 + child: Padding(
33 + padding: const EdgeInsets.symmetric(horizontal: 18.0),
34 + child: Observer(
35 + builder: (_) => Column(
36 + children: [
37 + Padding(
38 + padding: const EdgeInsets.only(bottom: 12.0),
39 + child: Material(
40 + color: Colors.transparent,
41 + child: SelectDeselectAllBar(
42 + title: S.of(context).swap_providers,
43 + onSelected: (val) {
44 + if ((val &&
45 + dashboardViewModel.tradeFilterStore.enabledProviders == 0) ||
46 + (!val &&
47 + dashboardViewModel.tradeFilterStore.enabledProviders > 0)) {
48 + dashboardViewModel.tradeFilterStore
49 + .toggleDisplayExchange(ExchangeProviderDescription.all);
50 + }
51 + }),
52 + ),
53 + ),
54 + NewListSections(sections: {
55 + "": dashboardViewModel.exchangeFilterItems
56 + .whereType<SwapProviderFilterItem>()
57 + .map((item) => ListItemCheckbox(
58 + iconPath: item.providerDescription.image,
59 + keyValue: item.caption,
60 + label: item.caption,
61 + value: item.value(),
62 + onChanged: (val) => dashboardViewModel.tradeFilterStore
63 + .toggleDisplayExchange(item.providerDescription)))
64 + .toList()
65 + }),
66 + ],
67 + ),
68 + ),
69 + ),
70 + ))
71 + ],
72 + ),
73 + );
74 + }
75 +}
lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart
+2 -2
@@ -87,7 +87,7 @@ class HistoryTileBase extends StatelessWidget {
87 bottomLeft: Radius.circular(roundedBottom ? 22 : 0),
88 bottomRight: Radius.circular(roundedBottom ? 22 : 0),
89 )),
90 - color: Theme.of(context).colorScheme.onInverseSurface,
90 + color: Theme.of(context).colorScheme.surfaceContainer,
91 ),
92 child: Padding(
93 padding: const EdgeInsets.symmetric(
@@ -151,7 +151,7 @@ class HistoryTileBase extends StatelessWidget {
151 ),
152 ),
153 if(bottomSeparator) Container(
154 - color: Theme.of(context).colorScheme.onInverseSurface,
154 + color: Theme.of(context).colorScheme.surfaceContainer,
155 child: Padding(
156 padding: EdgeInsets.only(left: 56, right: 16),
157 child: Container(height: 1, color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175)),
lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart new
+56
@@ -0,0 +1,56 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 +import 'package:flutter/material.dart';
4 +
5 +class HistoryTopBar extends StatelessWidget {
6 + const HistoryTopBar({super.key, required this.onTap});
7 +
8 + final VoidCallback onTap;
9 +
10 + @override
11 + Widget build(BuildContext context) {
12 + return SliverToBoxAdapter(
13 + child: GestureDetector(
14 + onTap: onTap,
15 + behavior: HitTestBehavior.opaque,
16 + child: Padding(
17 + padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
18 + child: Container(
19 + decoration: BoxDecoration(
20 + borderRadius: BorderRadius.vertical(
21 + top: Radius.circular(18),
22 + ),
23 + color: Theme.of(context).colorScheme.surfaceContainer),
24 + child: Padding(
25 + padding: EdgeInsets.symmetric(
26 + horizontal: 12,
27 + ),
28 + child: Column(
29 + spacing: 12,
30 + children: [
31 + SizedBox.shrink(),
32 + Row(
33 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
34 + children: [
35 + Text(S.of(context).history),
36 + CakeImageWidget(
37 + imageUrl: "assets/new-ui/arrow_right.svg",
38 + colorFilter: ColorFilter.mode(
39 + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
40 + )
41 + ],
42 + ),
43 + Container(
44 + height: 1,
45 + width: double.infinity,
46 + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
47 + )
48 + ],
49 + ),
50 + ),
51 + ),
52 + ),
53 + ),
54 + );
55 + }
56 +}
lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+135 -141
@@ -14,6 +14,7 @@ import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
14 import 'package:flutter/material.dart';
15 import 'package:flutter/services.dart';
16 import 'package:flutter_mobx/flutter_mobx.dart';
17 +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
18
19 class TransactionDetailsModal extends StatefulWidget {
20 const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false});
@@ -47,160 +48,153 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
48
49 @override
50 Widget build(BuildContext context) {
50 - return DraggableScrollableSheet(
51 - expand: false,
52 - initialChildSize: 0.9,
53 - minChildSize: 0.25,
54 - maxChildSize: 0.9,
55 - snap: true,
56 - snapSizes: const [0.9],
57 - builder: (context, controller) => SafeArea(
58 - bottom: false,
59 - child: Padding(
60 - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
61 - child: GestureDetector(
62 - onTap: FocusScope.of(context).unfocus,
63 - child: Container(
64 - decoration: BoxDecoration(
65 - color: Theme.of(context).colorScheme.surface,
66 - borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
51 + return SafeArea(
52 + bottom: false,
53 + child: Padding(
54 + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
55 + child: GestureDetector(
56 + onTap: FocusScope.of(context).unfocus,
57 + child: Container(
58 + decoration: BoxDecoration(
59 + color: Theme.of(context).colorScheme.surface,
60 + borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
61 + child: Column(
62 + children: [
63 + ModalTopBar(
64 + title: S.of(context).transaction,
65 + leadingIcon: Icon(Icons.close),
66 + onLeadingPressed: Navigator.of(context).pop,
67 + ),
68 + Expanded(
69 + child: SingleChildScrollView(
70 + controller: ModalScrollController.of(context),
71 child: Column(
72 children: [
69 - ModalTopBar(
70 - title: S.of(context).transaction,
71 - leadingIcon: Icon(Icons.close),
72 - onLeadingPressed: Navigator.of(context).pop,
73 + TokenImageWidget(
74 + imageUrl: widget
75 + .transactionDetailsViewModel.transactionAsset.iconPath ??
76 + "",
77 + size: 64,
78 ),
74 - Expanded(
75 - child: SingleChildScrollView(
76 - controller: controller,
77 - child: Column(
78 - children: [
79 - TokenImageWidget(
80 - imageUrl: widget
81 - .transactionDetailsViewModel.transactionAsset.iconPath ??
82 - "",
83 - size: 64,
84 - ),
85 - SizedBox(height: 10),
86 - Text(
87 - widget.transactionDetailsViewModel.formattedTitle +
88 - widget.transactionDetailsViewModel.formattedStatus,
89 - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
90 - ),
91 - CopyWrapper(
92 - requireLongPress: true,
93 - data: ClipboardData(text: widget.transactionDetailsViewModel.formattedCryptoAmount),
94 - builder: (context, copied)=> AnimatedSwitcher(
95 - duration: Duration(milliseconds: 300),
96 - child: Text(
97 - key: ValueKey(copied),
98 - copied ? S.of(context).copied : widget.transactionDetailsViewModel.formattedCryptoAmount,
99 - style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface),
100 - ),
101 - ),
102 - ),
103 - Padding(
104 - padding: const EdgeInsets.all(16.0),
105 - child: Column(
106 - spacing: 12,
107 - children: [
108 - NewListSections(sections: {
109 - "": widget.transactionDetailsViewModel.items
110 - .map((item) {
111 - if (item.value.isEmpty) return null;
79 + SizedBox(height: 10),
80 + Text(
81 + widget.transactionDetailsViewModel.formattedTitle +
82 + widget.transactionDetailsViewModel.formattedStatus,
83 + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
84 + ),
85 + CopyWrapper(
86 + requireLongPress: true,
87 + data: ClipboardData(text: widget.transactionDetailsViewModel.formattedCryptoAmount),
88 + builder: (context, copied)=> AnimatedSwitcher(
89 + duration: Duration(milliseconds: 300),
90 + child: Text(
91 + key: ValueKey(copied),
92 + copied ? S.of(context).copied : widget.transactionDetailsViewModel.formattedCryptoAmount,
93 + style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface),
94 + ),
95 + ),
96 + ),
97 + Padding(
98 + padding: const EdgeInsets.all(16.0),
99 + child: Column(
100 + spacing: 12,
101 + children: [
102 + NewListSections(sections: {
103 + "": widget.transactionDetailsViewModel.items
104 + .map((item) {
105 + if (item.value.isEmpty) return null;
106
113 - final shouldBuildBottomWidget =
114 - item.value.length > 25;
107 + final shouldBuildBottomWidget =
108 + item.value.length > 25;
109
116 - return ListItemRegularRow(
117 - copyableText: item.value,
118 - showArrow: false,
119 - keyValue:
120 - ((item.key as ValueKey?)?.value as String?) ??
121 - item.title,
122 - label: item.title,
123 - trailingWidget: shouldBuildBottomWidget
124 - ? null
125 - : _buildTrailingWIdget(item),
126 - bottomWidget: shouldBuildBottomWidget
127 - ? _buildBottomWidget(item)
128 - : null);
129 - })
130 - .whereType<ListItem>()
131 - .toList(),
132 - }),
133 - Container(
134 - decoration: BoxDecoration(
135 - borderRadius: BorderRadius.circular(20),
136 - color: Theme.of(context).colorScheme.surfaceContainer),
137 - child: Padding(
138 - padding: const EdgeInsets.all(12.0),
139 - child: Column(
140 - spacing: 8,
141 - crossAxisAlignment: CrossAxisAlignment.start,
142 - children: [
143 - Text(S.of(context).note),
144 - TextField(
145 - focusNode: noteFocusNode,
146 - controller: noteController,
147 - decoration: InputDecoration(
148 - hintText: S.of(context).add_a_note,
149 - border: InputBorder.none,
150 - focusedBorder: InputBorder.none,
151 - enabledBorder: InputBorder.none,
152 - contentPadding: EdgeInsets.zero,
153 - isDense: true),
154 - )
155 - ],
156 - ),
157 - ),
158 - ),
159 - Observer(
160 - builder: (_) => NewListSections(sections: {
161 - "view tx": [
162 - ListItemRegularRow(
163 - keyValue: "view tx on",
164 - label: widget.transactionDetailsViewModel
165 - .explorerDescription,
166 - onTap: widget
167 - .transactionDetailsViewModel.launchExplorer,
168 - foregroundColor:
169 - Theme.of(context).colorScheme.primary,
170 - trailingIconPath: "assets/new-ui/link_arrow.svg",
171 - trailingIconSize: 8)
172 - ],
173 - if (widget.transactionDetailsViewModel.canReplaceByFee)
174 - "rbf": [
175 - ListItemRegularRow(
176 - keyValue: "replace by fee",
177 - label: S.of(context).bump_fee,
178 - onTap: () {
179 - Navigator.of(context)
180 - .pushNamed(Routes.bumpFeePage, arguments: [
181 - widget.transactionDetailsViewModel
182 - .transactionInfo,
183 - widget.transactionDetailsViewModel
184 - .rawTransaction
185 - ]);
186 - })
187 - ]
188 - }),
110 + return ListItemRegularRow(
111 + copyableText: item.value,
112 + showArrow: false,
113 + keyValue:
114 + ((item.key as ValueKey?)?.value as String?) ??
115 + item.title,
116 + label: item.title,
117 + trailingWidget: shouldBuildBottomWidget
118 + ? null
119 + : _buildTrailingWIdget(item),
120 + bottomWidget: shouldBuildBottomWidget
121 + ? _buildBottomWidget(item)
122 + : null);
123 + })
124 + .whereType<ListItem>()
125 + .toList(),
126 + }),
127 + Container(
128 + decoration: BoxDecoration(
129 + borderRadius: BorderRadius.circular(20),
130 + color: Theme.of(context).colorScheme.surfaceContainer),
131 + child: Padding(
132 + padding: const EdgeInsets.all(12.0),
133 + child: Column(
134 + spacing: 8,
135 + crossAxisAlignment: CrossAxisAlignment.start,
136 + children: [
137 + Text(S.of(context).note),
138 + TextField(
139 + focusNode: noteFocusNode,
140 + controller: noteController,
141 + decoration: InputDecoration(
142 + hintText: S.of(context).add_a_note,
143 + border: InputBorder.none,
144 + focusedBorder: InputBorder.none,
145 + enabledBorder: InputBorder.none,
146 + contentPadding: EdgeInsets.zero,
147 + isDense: true),
148 )
149 ],
150 ),
151 ),
193 - SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
194 - ],
195 - ),
152 + ),
153 + Observer(
154 + builder: (_) => NewListSections(sections: {
155 + "view tx": [
156 + ListItemRegularRow(
157 + keyValue: "view tx on",
158 + label: widget.transactionDetailsViewModel
159 + .explorerDescription,
160 + onTap: widget
161 + .transactionDetailsViewModel.launchExplorer,
162 + foregroundColor:
163 + Theme.of(context).colorScheme.primary,
164 + trailingIconPath: "assets/new-ui/link_arrow.svg",
165 + trailingIconSize: 8)
166 + ],
167 + if (widget.transactionDetailsViewModel.canReplaceByFee)
168 + "rbf": [
169 + ListItemRegularRow(
170 + keyValue: "replace by fee",
171 + label: S.of(context).bump_fee,
172 + onTap: () {
173 + Navigator.of(context)
174 + .pushNamed(Routes.bumpFeePage, arguments: [
175 + widget.transactionDetailsViewModel
176 + .transactionInfo,
177 + widget.transactionDetailsViewModel
178 + .rawTransaction
179 + ]);
180 + })
181 + ]
182 + }),
183 + )
184 + ],
185 ),
197 - )
186 + ),
187 + SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
188 ],
189 ),
190 ),
201 - ),
202 - ),
203 - ));
191 + )
192 + ],
193 + ),
194 + ),
195 + ),
196 + ),
197 + );
198 }
199
200 Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
lib/new-ui/widgets/select_deselect_all.dart new
+39
@@ -0,0 +1,39 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:flutter/material.dart';
3 +
4 +class SelectDeselectAllBar extends StatelessWidget {
5 + const SelectDeselectAllBar({super.key, required this.title, required this.onSelected});
6 +
7 + final String title;
8 + final Function(bool) onSelected;
9 +
10 + @override
11 + Widget build(BuildContext context) {
12 + return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
13 + Text(
14 + title,
15 + style: TextStyle(
16 + fontSize: 14,
17 + fontWeight: FontWeight.w400,
18 + color: Theme.of(context).colorScheme.onSurfaceVariant),
19 + ),
20 + Row(
21 + spacing: 20,
22 + children: [
23 + GestureDetector(
24 + onTap: () => onSelected(true),
25 + child: Text(
26 + S.of(context).select_all,
27 + style: TextStyle(color: Theme.of(context).colorScheme.primary),
28 + ),
29 + ),
30 + GestureDetector(
31 + onTap: () => onSelected(false),
32 + child: Text(S.of(context).unselect_all,
33 + style: TextStyle(color: Theme.of(context).colorScheme.primary)),
34 + )
35 + ],
36 + )
37 + ]);
38 + }
39 +}
lib/router.dart
+1 -1
@@ -679,7 +679,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
679 return MaterialPageRoute<void>(builder: (_) => getIt.get<ExchangeConfirmPage>());
680
681 case Routes.tradeDetails:
682 - return MaterialPageRoute<void>(
682 + return CupertinoPageRoute<void>(
683 fullscreenDialog: true,
684 builder: (_) => getIt.get<TradeDetailsPage>(param1: settings.arguments as Trade));
685
lib/src/screens/dashboard/widgets/header_row.dart
-5
@@ -62,11 +62,6 @@ class HeaderRow extends StatelessWidget {
62 child: GestureDetector(
63 key: ValueKey('transactions_page_header_row_transaction_filter_button_key'),
64 onTap: () {
65 - showPopUp<void>(
66 - context: context,
67 - builder: (context) =>
68 - FilterWidget(filterItems: dashboardViewModel.filterItems),
69 - );
65 },
66 child: Semantics(
67 label: 'Transaction Filter',
lib/src/widgets/new_list_row/list_item_checkbox_widget.dart
+3 -1
@@ -11,6 +11,7 @@ class ListItemCheckboxWidget extends StatefulWidget {
11 required this.label,
12 required this.value,
13 required this.onChanged,
14 + this.subtitleColor,
15 this.onTap,
16 this.isFirstInSection = false,
17 this.isLastInSection = false, this.subtitle, this.iconPath, this.showArrow = false,
@@ -22,6 +23,7 @@ class ListItemCheckboxWidget extends StatefulWidget {
23 final String? iconPath;
24 final bool showArrow;
25 final bool value;
26 + final Color? subtitleColor;
27 final VoidCallback? onTap;
28 final ValueChanged<bool> onChanged;
29 final bool isFirstInSection;
@@ -81,7 +83,7 @@ class _ListItemCheckboxWidgetState extends State<ListItemCheckboxWidget> {
83 Text(
84 widget.subtitle!,
85 style: TextStyle(
84 - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
86 + fontSize: 12, color: widget.subtitleColor ?? Theme.of(context).colorScheme.onSurfaceVariant),
87 )
88 ],
89 ),
lib/src/widgets/new_list_row/new_list_section.dart
+1
@@ -133,6 +133,7 @@ class NewListSections extends StatelessWidget {
133 label: item.label,
134 subtitle: item.subtitle,
135 iconPath: item.iconPath,
136 + subtitleColor: item.subtitleColor,
137 showArrow: item.showArrow,
138 onTap: item.onTap,
139 value: item.value,
lib/store/dashboard/trade_filter_store.dart
+19
@@ -69,6 +69,25 @@ abstract class TradeFilterStoreBase with Store {
69 @observable
70 bool displayNearIntents;
71
72 + @computed
73 + int get enabledProviders => [
74 + displayXMRTO,
75 + displayChangeNow,
76 + displaySideShift,
77 + displayMorphToken,
78 + displaySimpleSwap,
79 + displayTrocador,
80 + displayExolix,
81 + displayChainflip,
82 + displayThorChain,
83 + displayLetsExchange,
84 + displayStealthEx,
85 + displayXOSwap,
86 + displaySwapTrade,
87 + displaySwapXyz,
88 + displayNearIntents
89 + ].where((item) => item).length;
90 +
91 @computed
92 bool get displayAllTrades =>
93 displayChangeNow &&
lib/view_model/dashboard/dashboard_view_model.dart
+115 -106
@@ -96,111 +96,104 @@ abstract class DashboardViewModelBase with Store {
96 isShowFirstYatIntroduction = false,
97 isShowSecondYatIntroduction = false,
98 isShowThirdYatIntroduction = false,
99 - filterItems = {
100 - S.current.transactions: [
99 + filterItems = [
100 + // FilterItem(
101 + // value: () => transactionFilterStore.displayAll,
102 + // caption: S.current.all_transactions,
103 + // onChanged: transactionFilterStore.toggleAll),
104 + FilterItem(
105 + value: () => transactionFilterStore.displayIncoming,
106 + caption: S.current.send,
107 + onChanged: transactionFilterStore.toggleIncoming),
108 + FilterItem(
109 + value: () => transactionFilterStore.displayOutgoing,
110 + caption: S.current.receive,
111 + onChanged: transactionFilterStore.toggleOutgoing),
112 + if (appStore.wallet!.type == WalletType.bitcoin)
113 FilterItem(
102 - value: () => transactionFilterStore.displayAll,
103 - caption: S.current.all_transactions,
104 - onChanged: transactionFilterStore.toggleAll),
105 - FilterItem(
106 - value: () => transactionFilterStore.displayIncoming,
107 - caption: S.current.incoming,
108 - onChanged: transactionFilterStore.toggleIncoming),
109 - FilterItem(
110 - value: () => transactionFilterStore.displayOutgoing,
111 - caption: S.current.outgoing,
112 - onChanged: transactionFilterStore.toggleOutgoing),
113 - if (appStore.wallet!.type == WalletType.bitcoin)
114 - FilterItem(
115 - value: () => transactionFilterStore.displaySilentPayments,
116 - caption: S.current.silent_payments,
117 - onChanged: transactionFilterStore.toggleSilentPayments,
118 - ),
119 - // FilterItem(
120 - // value: () => false,
121 - // caption: S.current.transactions_by_date,
122 - // onChanged: null),
123 - ],
124 - 'Orders': [
125 - FilterItem(
126 - value: () => orderFilterStore.displayCakePay,
127 - caption: 'Cake Pay',
128 - onChanged: () =>
129 - orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
130 - ],
131 - S.current.trades: [
132 - FilterItem(
133 - value: () => tradeFilterStore.displayAllTrades,
134 - caption: S.current.all_trades,
135 - onChanged: () =>
136 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
137 - FilterItem(
138 - value: () => tradeFilterStore.displayChangeNow,
139 - caption: ExchangeProviderDescription.changeNow.title,
140 - onChanged: () =>
141 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
142 - FilterItem(
143 - value: () => tradeFilterStore.displaySideShift,
144 - caption: ExchangeProviderDescription.sideShift.title,
145 - onChanged: () =>
146 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
147 - FilterItem(
148 - value: () => tradeFilterStore.displaySimpleSwap,
149 - caption: ExchangeProviderDescription.simpleSwap.title,
150 - onChanged: () =>
151 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
152 - FilterItem(
153 - value: () => tradeFilterStore.displayTrocador,
154 - caption: ExchangeProviderDescription.trocador.title,
155 - onChanged: () =>
156 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
157 - FilterItem(
158 - value: () => tradeFilterStore.displayExolix,
159 - caption: ExchangeProviderDescription.exolix.title,
160 - onChanged: () =>
161 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
162 - FilterItem(
163 - value: () => tradeFilterStore.displayChainflip,
164 - caption: ExchangeProviderDescription.chainflip.title,
165 - onChanged: () =>
166 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)),
167 - FilterItem(
168 - value: () => tradeFilterStore.displayThorChain,
169 - caption: ExchangeProviderDescription.thorChain.title,
170 - onChanged: () =>
171 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
172 - FilterItem(
173 - value: () => tradeFilterStore.displayLetsExchange,
174 - caption: ExchangeProviderDescription.letsExchange.title,
175 - onChanged: () => tradeFilterStore
176 - .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
177 - FilterItem(
178 - value: () => tradeFilterStore.displayStealthEx,
179 - caption: ExchangeProviderDescription.stealthEx.title,
180 - onChanged: () =>
181 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
182 - FilterItem(
183 - value: () => tradeFilterStore.displayXOSwap,
184 - caption: ExchangeProviderDescription.xoSwap.title,
185 - onChanged: () =>
186 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
187 - FilterItem(
188 - value: () => tradeFilterStore.displaySwapTrade,
189 - caption: ExchangeProviderDescription.swapTrade.title,
190 - onChanged: () =>
191 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
192 - FilterItem(
193 - value: () => tradeFilterStore.displaySwapXyz,
194 - caption: ExchangeProviderDescription.swapsXyz.title,
195 - onChanged: () =>
196 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
197 - FilterItem(
198 - value: () => tradeFilterStore.displayNearIntents,
199 - caption: ExchangeProviderDescription.nearIntents.title,
200 - onChanged: () => tradeFilterStore
201 - .toggleDisplayExchange(ExchangeProviderDescription.nearIntents)),
202 - ]
203 - },
114 + value: () => transactionFilterStore.displaySilentPayments,
115 + caption: S.current.silent_payments,
116 + onChanged: transactionFilterStore.toggleSilentPayments,
117 + ),
118 + SwapFilterItem(
119 + enabledProviders: () => tradeFilterStore.enabledProviders,
120 + allEnabled: () => tradeFilterStore.displayAllTrades,
121 + value: () => tradeFilterStore.displayAllTrades,
122 + onChanged: () =>
123 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
124 + FilterItem(
125 + value: () => orderFilterStore.displayCakePay,
126 + caption: 'Cake Pay',
127 + onChanged: () =>
128 + orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
129 + ],
130 + exchangeFilterItems = [
131 + SwapProviderFilterItem(
132 + providerDescription: ExchangeProviderDescription.changeNow,
133 + value: () => tradeFilterStore.displayChangeNow,
134 + onChanged: () =>
135 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
136 + SwapProviderFilterItem(
137 + providerDescription: ExchangeProviderDescription.sideShift,
138 + value: () => tradeFilterStore.displaySideShift,
139 + onChanged: () =>
140 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
141 + SwapProviderFilterItem(
142 + providerDescription: ExchangeProviderDescription.simpleSwap,
143 + value: () => tradeFilterStore.displaySimpleSwap,
144 + onChanged: () =>
145 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
146 + SwapProviderFilterItem(
147 + providerDescription: ExchangeProviderDescription.trocador,
148 + value: () => tradeFilterStore.displayTrocador,
149 + onChanged: () =>
150 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
151 + SwapProviderFilterItem(
152 + providerDescription: ExchangeProviderDescription.exolix,
153 + value: () => tradeFilterStore.displayExolix,
154 + onChanged: () =>
155 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
156 + SwapProviderFilterItem(
157 + providerDescription: ExchangeProviderDescription.chainflip,
158 + value: () => tradeFilterStore.displayChainflip,
159 + onChanged: () =>
160 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)),
161 + SwapProviderFilterItem(
162 + providerDescription: ExchangeProviderDescription.thorChain,
163 + value: () => tradeFilterStore.displayThorChain,
164 + onChanged: () =>
165 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
166 + SwapProviderFilterItem(
167 + providerDescription: ExchangeProviderDescription.letsExchange,
168 + value: () => tradeFilterStore.displayLetsExchange,
169 + onChanged: () =>
170 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
171 + SwapProviderFilterItem(
172 + providerDescription: ExchangeProviderDescription.stealthEx,
173 + value: () => tradeFilterStore.displayStealthEx,
174 + onChanged: () =>
175 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
176 + SwapProviderFilterItem(
177 + providerDescription: ExchangeProviderDescription.xoSwap,
178 + value: () => tradeFilterStore.displayXOSwap,
179 + onChanged: () =>
180 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
181 + SwapProviderFilterItem(
182 + providerDescription: ExchangeProviderDescription.swapTrade,
183 + value: () => tradeFilterStore.displaySwapTrade,
184 + onChanged: () =>
185 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
186 + SwapProviderFilterItem(
187 + providerDescription: ExchangeProviderDescription.swapsXyz,
188 + value: () => tradeFilterStore.displaySwapXyz,
189 + onChanged: () =>
190 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
191 + SwapProviderFilterItem(
192 + providerDescription: ExchangeProviderDescription.nearIntents,
193 + value: () => tradeFilterStore.displayNearIntents,
194 + onChanged: () =>
195 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)),
196 + ],
197 subname = '',
198 name = appStore.wallet!.name,
199 type = appStore.wallet!.type,
@@ -406,6 +399,15 @@ abstract class DashboardViewModelBase with Store {
399 return false;
400 }
401
402 + @action
403 + void changeAllFilterItems(bool value) {
404 + for (final item in filterItems) {
405 + if (item.value() != value) {
406 + item.onChanged();
407 + }
408 + }
409 + }
410 +
411 @action
412 Future<void> loadCardDesigns() async {
413 final accountStyleSettings =
@@ -719,7 +721,10 @@ abstract class DashboardViewModelBase with Store {
721 static const shortHistoryLength = 3;
722
723 @computed
722 - List<ActionListItem> get itemsShort => items.where((item)=>item is! DateSectionItem).take(3).toList();
724 + List<ActionListItem> get itemsShort => items
725 + .where((item) => item is! DateSectionItem)
726 + .take(shortHistoryLength)
727 + .toList();
728
729 @observable
730 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@@ -1129,7 +1134,11 @@ abstract class DashboardViewModelBase with Store {
1134
1135 PayjoinTransactionsStore payjoinTransactionsStore;
1136
1132 - Map<String, List<FilterItem>> filterItems;
1137 + // Map<String, List<FilterItem>> filterItems;
1138 +
1139 + List<FilterItem> filterItems;
1140 +
1141 + List<FilterItem> exchangeFilterItems;
1142
1143 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
1144
lib/view_model/dashboard/date_section_item.dart
+30 -2
@@ -1,8 +1,36 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
3
4 class DateSectionItem extends ActionListItem {
5 DateSectionItem(this.date, {required super.key});
5 -
6 +
7 @override
8 final DateTime date;
8 -}
\ No newline at end of file
9 +}
10 +
11 +abstract class SpecificDateSectionItem extends DateSectionItem {
12 + SpecificDateSectionItem(super.date, {required super.key});
13 +
14 + String get text;
15 +}
16 +
17 +class Last30daysTransactionItem extends SpecificDateSectionItem {
18 + Last30daysTransactionItem(super.date, {required super.key});
19 +
20 + @override
21 + String get text => S.current.last_30_days;
22 +}
23 +
24 +class Last7daysTransactionItem extends SpecificDateSectionItem {
25 + Last7daysTransactionItem(super.date, {required super.key});
26 +
27 + @override
28 + String get text => S.current.last_7_days;
29 +}
30 +
31 +class TodayTransactionItem extends SpecificDateSectionItem {
32 + TodayTransactionItem(super.date, {required super.key});
33 +
34 + @override
35 + String get text => S.current.today;
36 +}
lib/view_model/dashboard/filter_item.dart
+29 -1
@@ -1,4 +1,4 @@
1 -import 'package:mobx/mobx.dart';
1 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2
3 class FilterItem {
4 FilterItem({
@@ -9,4 +9,32 @@ class FilterItem {
9 bool Function() value;
10 String caption;
11 Function onChanged;
12 +}
13 +
14 +class SwapFilterItem extends FilterItem {
15 + SwapFilterItem({
16 + required this.enabledProviders,
17 + required this.allEnabled,
18 + required super.value,
19 + super.caption = "Swap",
20 + required super.onChanged,
21 + });
22 +
23 + int Function() enabledProviders;
24 + bool Function() allEnabled;
25 +}
26 +
27 +
28 +class SwapProviderFilterItem extends FilterItem {
29 + SwapProviderFilterItem({
30 + required super.value,
31 + required super.onChanged,
32 + required this.providerDescription,
33 + super.caption = ""
34 +});
35 +
36 + @override
37 + String get caption => providerDescription.title;
38 +
39 + final ExchangeProviderDescription providerDescription;
40 }
\ No newline at end of file
lib/view_model/dashboard/formatted_item_list.dart
+69 -32
@@ -1,45 +1,82 @@
1 -import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
2 -import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
3 -import 'package:flutter/foundation.dart';
1 +import "package:cake_wallet/view_model/dashboard/action_list_item.dart";
2 +import "package:cake_wallet/view_model/dashboard/date_section_item.dart";
3 +import "package:flutter/foundation.dart";
4 +
5 +enum _DateBucket { recent, last7Days, last30Days, byMonth }
6
7 List<ActionListItem> formattedItemsList(List<ActionListItem> items) {
8 final formattedList = <ActionListItem>[];
7 - DateTime? lastDate;
9 items.sort((a, b) => b.date.compareTo(a.date));
10
10 - for (var i = 0; i < items.length; i++) {
11 - final transaction = items[i];
12 -
13 - if (lastDate == null) {
14 - lastDate = transaction.date;
15 - formattedList.add(
16 - DateSectionItem(
17 - transaction.date,
18 - key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
19 - ),
20 - );
21 - formattedList.add(transaction);
22 - continue;
23 - }
11 + final now = DateTime.now();
12 + final last24hThreshold = now.subtract(const Duration(hours: 24));
13 + final last7daysThreshold = now.subtract(const Duration(days: 7));
14 + final last30daysThreshold = now.subtract(const Duration(days: 30));
15 +
16 + _DateBucket? lastBucket;
17 + DateTime? lastMonthDate;
18 +
19 + for (final transaction in items) {
20 + final date = transaction.date;
21
25 - final isCurrentDay = lastDate.year == transaction.date.year &&
26 - lastDate.month == transaction.date.month &&
27 - lastDate.day == transaction.date.day;
22 + final bucket = date.isAfter(last24hThreshold)
23 + ? _DateBucket.recent
24 + : date.isAfter(last7daysThreshold)
25 + ? _DateBucket.last7Days
26 + : date.isAfter(last30daysThreshold)
27 + ? _DateBucket.last30Days
28 + : _DateBucket.byMonth;
29
29 - if (isCurrentDay) {
30 - formattedList.add(transaction);
31 - continue;
30 + switch (bucket) {
31 + case _DateBucket.recent:
32 + if (lastBucket != _DateBucket.recent) {
33 + formattedList.add(
34 + TodayTransactionItem(
35 + date,
36 + key: const ValueKey("last_7_days_section_item_key"),
37 + ),
38 + );
39 + }
40 + break;
41 + case _DateBucket.last7Days:
42 + if (lastBucket != _DateBucket.last7Days) {
43 + formattedList.add(
44 + Last7daysTransactionItem(
45 + date,
46 + key: const ValueKey("last_7_days_section_item_key"),
47 + ),
48 + );
49 + }
50 + break;
51 + case _DateBucket.last30Days:
52 + if (lastBucket != _DateBucket.last30Days) {
53 + formattedList.add(
54 + Last30daysTransactionItem(
55 + date,
56 + key: const ValueKey("last_30_days_section_item_key"),
57 + ),
58 + );
59 + }
60 + break;
61 + case _DateBucket.byMonth:
62 + final isNewMonth = lastMonthDate == null ||
63 + lastMonthDate.year != date.year ||
64 + lastMonthDate.month != date.month;
65 + if (isNewMonth) {
66 + lastMonthDate = date;
67 + formattedList.add(
68 + DateSectionItem(
69 + date,
70 + key: ValueKey("date_section_item_${date.year}_${date.month}_key"),
71 + ),
72 + );
73 + }
74 + break;
75 }
76
34 - lastDate = transaction.date;
35 - formattedList.add(
36 - DateSectionItem(
37 - transaction.date,
38 - key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
39 - ),
40 - );
77 + lastBucket = bucket;
78 formattedList.add(transaction);
79 }
80
81 return formattedList;
45 -}
\ No newline at end of file
82 +}
res/values/strings_en.arb
+6
@@ -53,6 +53,7 @@
53 "alert_notice": "Notice",
54 "all": "ALL",
55 "all_coins": "All Coins",
56 + "all_pascal_case": "All",
57 "all_trades": "All trades",
58 "all_transactions": "All transactions",
59 "alphabetical": "Alphabetical",
@@ -487,6 +488,7 @@
488 "file_saved": "File saved",
489 "fill_code": "Please fill in the verification code provided to your email",
490 "filter_by": "Filter by",
491 + "filter_swap_providers": "Filter Swap Providers",
492 "filters": "Filters",
493 "finding_provider": "Finding provider",
494 "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven",
@@ -568,6 +570,7 @@
570 "label": "Label",
571 "label_address": "Label address",
572 "last_30_days": "Last 30 days",
573 + "last_7_days": "Last 7 days",
574 "learn_more": "Learn More",
575 "ledger_connection_error": "Failed to connect to your Ledger. Please try again.",
576 "ledger_error_device_locked": "The Ledger is locked",
@@ -682,6 +685,7 @@
685 "no_id_needed": "No ID needed!",
686 "no_id_required": "No ID required. Top up and spend anywhere",
687 "no_providers_available": "No providers available",
688 + "no_providers_selected": "No Providers Selected",
689 "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use.",
690 "no_relays": "No relays",
691 "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.",
@@ -871,6 +875,7 @@
875 "rescan": "Rescan",
876 "resend_code": "Please resend it",
877 "reset": "Reset",
878 + "reset_filters": "Reset Filters",
879 "reset_password": "Reset Password",
880 "restore": "Restore",
881 "restore_active_seed": "Active seed",
@@ -1285,6 +1290,7 @@
1290 "tx_wrong_balance_exception": "You do not have enough ${currency} to send this amount.",
1291 "tx_wrong_balance_with_amount_exception": "You do not have enough ${currency} to send the total amount of ${amount}",
1292 "tx_zero_fee_exception": "Cannot send transaction with 0 fee. Try increasing the rate or checking your connection for latest estimates.",
1293 + "type": "Type",
1294 "unavailable_balance": "Unavailable balance",
1295 "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.",
1296 "unconfirmed": "Unconfirmed Balance",