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",