add copy indicator on tx details screen (#3152)
malik1004x committed
Apr 14, 2026 at 05:45 UTC
c338c6d6b8c5bc0f64e2044e490b068a5f969d86
6 files changed
+162
-77
lib/entities/new_ui_entities/list_item/list_item_regular_row.dart
+3
-1
@@ -15,13 +15,15 @@ class ListItemRegularRow extends ListItem {
15
this.trailingWidget,
16
this.truncateTrailingText = false,
17
this.foregroundColor,
18
- this.trailingIconSize
18
+ this.trailingIconSize,
19
+ this.copyableText
20
});
21
22
final String? subtitle;
23
final String? trailingText;
24
final String? iconPath;
25
final String? trailingIconPath;
26
+ final String? copyableText;
27
final VoidCallback? onTap;
28
final bool showArrow;
29
final Widget? bottomWidget;
lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+1
-4
@@ -99,10 +99,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
99
item.value.length > 25;
100
101
return ListItemRegularRow(
102
- onTap: () {
103
- Clipboard.setData(
104
- ClipboardData(text: item.value));
105
- },
102
+ copyableText: item.value,
103
showArrow: false,
104
keyValue:
105
((item.key as ValueKey?)?.value as String?) ??
lib/new-ui/widgets/copy_wrapper.dart
new
+59
@@ -0,0 +1,59 @@
1
+import 'dart:io';
2
+
3
+import 'package:device_info_plus/device_info_plus.dart';
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter/services.dart';
6
+
7
+class CopyWrapper extends StatefulWidget {
8
+ const CopyWrapper(
9
+ {super.key,
10
+ this.data,
11
+ required this.builder,
12
+ this.duration = const Duration(milliseconds: 1200)});
13
+
14
+ final ClipboardData? data;
15
+ final Widget Function(BuildContext, bool) builder;
16
+ final Duration duration;
17
+
18
+ @override
19
+ State<CopyWrapper> createState() => _CopyWrapperState();
20
+}
21
+
22
+class _CopyWrapperState extends State<CopyWrapper> {
23
+ bool copied = false;
24
+
25
+ void handleCopy() async {
26
+ if (widget.data == null) return;
27
+ Clipboard.setData(widget.data!);
28
+ if (await shouldShowCopied()) {
29
+ setState(() => copied = true);
30
+ Future.delayed(widget.duration, () {
31
+ if (mounted) setState(() => copied = false);
32
+ });
33
+ }
34
+ }
35
+
36
+ @override
37
+ Widget build(BuildContext context) {
38
+ return GestureDetector(
39
+ behavior: HitTestBehavior.translucent,
40
+ onTap: handleCopy,
41
+ child: widget.builder(context, copied),
42
+ );
43
+ }
44
+
45
+ // android 13 (sdk 33) added a built-in "text was copied to clipboard" ui element
46
+ Future<bool> shouldShowCopied() async {
47
+ if (!Platform.isAndroid) return true;
48
+
49
+ try {
50
+ final deviceInfo = DeviceInfoPlugin();
51
+ final androidInfo = await deviceInfo.androidInfo;
52
+ final sdk = androidInfo.version.sdkInt;
53
+
54
+ return sdk < 33;
55
+ } catch (_) {
56
+ return true;
57
+ }
58
+ }
59
+}
lib/src/widgets/new_list_row/list_Item_style_wrapper.dart
+3
-1
@@ -6,6 +6,7 @@ class ListItemStyleWrapper extends StatelessWidget {
6
required this.isFirstInSection,
7
required this.isLastInSection,
8
required this.builder,
9
+ this.backgroundColor,
10
this.onTap,
11
this.iconPath,
12
this.height,
@@ -16,6 +17,7 @@ class ListItemStyleWrapper extends StatelessWidget {
17
final bool isLastInSection;
18
final double? height;
19
final VoidCallback? onTap;
20
+ final Color? backgroundColor;
21
final Widget Function(BuildContext context, TextStyle textStyle, TextStyle labelStyle) builder;
22
23
@override
@@ -52,7 +54,7 @@ class ListItemStyleWrapper extends StatelessWidget {
54
shape: RoundedSuperellipseBorder(
55
borderRadius: radius,
56
),
55
- color: theme.colorScheme.surfaceContainer,
57
+ color: backgroundColor ?? theme.colorScheme.surfaceContainer,
58
),
59
child: Material(
60
color: Colors.transparent,
lib/src/widgets/new_list_row/list_item_regular_row_widget.dart
+95
-71
@@ -1,7 +1,9 @@
1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
3
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
4
import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart';
5
import 'package:flutter/material.dart';
4
-import 'package:flutter_svg/svg.dart';
6
+import 'package:flutter/services.dart';
7
8
class ListItemRegularRowWidget extends StatelessWidget {
9
const ListItemRegularRowWidget({
@@ -20,7 +22,8 @@ class ListItemRegularRowWidget extends StatelessWidget {
22
this.foregroundColor,
23
this.trailingIconSize,
24
this.bottomWidget,
23
- this.trailingWidget
25
+ this.trailingWidget,
26
+ this.copyableText
27
});
28
29
final String keyValue;
@@ -38,6 +41,7 @@ class ListItemRegularRowWidget extends StatelessWidget {
41
final bool truncateTrailingText;
42
final Color? foregroundColor;
43
final double? trailingIconSize;
44
+ final String? copyableText;
45
46
@override
47
Widget build(BuildContext context) {
@@ -47,82 +51,102 @@ class ListItemRegularRowWidget extends StatelessWidget {
51
? "${trailingText!.substring(0, 17)}..."
52
: trailingText;
53
50
- return ListItemStyleWrapper(
51
- onTap: onTap,
52
- iconPath: iconPath,
53
- isFirstInSection: isFirstInSection,
54
- isLastInSection: isLastInSection,
55
- builder: (context, textStyle, labelStyle) {
56
- return Column(
57
- crossAxisAlignment: CrossAxisAlignment.start,
58
- mainAxisAlignment: MainAxisAlignment.center,
59
- children: [
60
- Row(
61
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
54
+ return CopyWrapper(
55
+ data: copyableText != null ? ClipboardData(text: copyableText!) : null,
56
+ builder: (context, copied) => AnimatedSwitcher(
57
+ duration: Duration(milliseconds: 150),
58
+ child: ListItemStyleWrapper(
59
+ key: ValueKey(copied),
60
+ backgroundColor: copied ? Theme.of(context).colorScheme.surfaceContainerHigh : null,
61
+ onTap: onTap,
62
+ iconPath: iconPath,
63
+ isFirstInSection: isFirstInSection,
64
+ isLastInSection: isLastInSection,
65
+ builder: (context, textStyle, labelStyle) {
66
+ return Column(
67
+ crossAxisAlignment: CrossAxisAlignment.start,
68
+ mainAxisAlignment: MainAxisAlignment.center,
69
children: [
63
- Expanded(
64
- child: Row(
65
- children: [
66
- if (iconPath != null)
67
- Padding(
68
- padding: const EdgeInsets.only(right: 12.0),
69
- child: CakeImageWidget(imageUrl: iconPath!, width: 24, height: 24,)
70
- ),
71
- Flexible(
72
- child: Column(
73
- mainAxisAlignment: MainAxisAlignment.center,
74
- crossAxisAlignment: CrossAxisAlignment.start,
75
- children: [
76
- Text(label,
77
- style: foregroundColor == null
78
- ? textStyle
79
- : textStyle.copyWith(color: foregroundColor)),
80
- if (subtitle != null)
81
- Text(
82
- subtitle!,
83
- style: labelStyle.copyWith(fontSize: 12),
84
- ),
85
- ],
86
- ),
87
- ),
88
- ],
89
- ),
90
- ),
70
Row(
71
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
72
children: [
93
- if (trailingTextToShow != null)
94
- Padding(
95
- padding: const EdgeInsets.only(right: 8.0),
96
- child: Text(
97
- trailingTextToShow,
98
- style: labelStyle,
99
- ),
73
+ Expanded(
74
+ child: Row(
75
+ children: [
76
+ if (iconPath != null)
77
+ Padding(
78
+ padding: const EdgeInsets.only(right: 12.0),
79
+ child: CakeImageWidget(
80
+ imageUrl: iconPath!,
81
+ width: 24,
82
+ height: 24,
83
+ )),
84
+ Flexible(
85
+ child: Column(
86
+ mainAxisAlignment: MainAxisAlignment.center,
87
+ crossAxisAlignment: CrossAxisAlignment.start,
88
+ children: [
89
+ if (copied)
90
+ Text(
91
+ S.of(context).copied,
92
+ style: textStyle.copyWith(
93
+ color: Theme.of(context).colorScheme.primary),
94
+ )
95
+ else
96
+ Text(label,
97
+ style: foregroundColor == null
98
+ ? textStyle
99
+ : textStyle.copyWith(color: foregroundColor)),
100
+ if (subtitle != null)
101
+ Text(
102
+ subtitle!,
103
+ style: labelStyle.copyWith(fontSize: 12),
104
+ ),
105
+ ],
106
+ ),
107
+ ),
108
+ ],
109
),
101
- if (trailingWidget != null)
102
- trailingWidget!
103
- else if (trailingIconPath != null)
104
- CakeImageWidget(imageUrl:
105
- trailingIconPath!,
106
- height: trailingIconSize ?? 18,
107
- width:trailingIconSize ?? 18,
108
- colorFilter: ColorFilter.mode(foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),
109
- )
110
- else if (showArrow)
111
- Padding(
112
- padding: const EdgeInsets.symmetric(vertical: 7.0),
113
- child: CakeImageWidget(imageUrl:
114
- "assets/new-ui/arrow_forward.svg",
115
- height: 14,
116
- color: theme.colorScheme.onSurfaceVariant
110
+ ),
111
+ Row(
112
+ children: [
113
+ if (trailingTextToShow != null)
114
+ Padding(
115
+ padding: const EdgeInsets.only(right: 8.0),
116
+ child: Text(
117
+ trailingTextToShow,
118
+ style: labelStyle,
119
+ ),
120
),
118
- )
121
+ if (trailingWidget != null)
122
+ trailingWidget!
123
+ else if (trailingIconPath != null)
124
+ CakeImageWidget(
125
+ imageUrl: trailingIconPath!,
126
+ height: trailingIconSize ?? 18,
127
+ width: trailingIconSize ?? 18,
128
+ colorFilter: ColorFilter.mode(
129
+ foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,
130
+ BlendMode.srcIn),
131
+ )
132
+ else if (showArrow)
133
+ Padding(
134
+ padding: const EdgeInsets.symmetric(vertical: 7.0),
135
+ child: CakeImageWidget(imageUrl:
136
+ "assets/new-ui/arrow_forward.svg",
137
+ height: 14,
138
+ color: theme.colorScheme.onSurfaceVariant
139
+ ),
140
+ )
141
+ ],
142
+ ),
143
],
144
),
145
+ if (bottomWidget != null) bottomWidget!
146
],
122
- ),
123
- if (bottomWidget != null) bottomWidget!
124
- ],
125
- );
126
- });
147
+ );
148
+ }),
149
+ ),
150
+ );
151
}
152
}
lib/src/widgets/new_list_row/new_list_section.dart
+1
@@ -103,6 +103,7 @@ class NewListSections extends StatelessWidget {
103
showArrow: item.showArrow,
104
truncateTrailingText: item.truncateTrailingText,
105
foregroundColor: item.foregroundColor,
106
+ copyableText: item.copyableText,
107
trailingIconSize: item.trailingIconSize,
108
trailingWidget: item.trailingWidget,
109
bottomWidget: item.bottomWidget,