ui: add copy button
Serhii committed
Jul 21, 2025 at 19:49 UTC
cff10f3a515dbf6de3fc278845adc254f3ddbe4f
2 files changed
+210
-42
lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart
+137
-24
@@ -4,6 +4,7 @@ import 'package:cake_wallet/utils/image_utill.dart';
4
import 'package:cake_wallet/view_model/send/output.dart';
5
import 'package:cw_core/crypto_currency.dart';
6
import 'package:flutter/material.dart';
7
+import 'package:flutter/services.dart';
8
9
class CakePayTransactionSentBottomSheet extends StatelessWidget {
10
const CakePayTransactionSentBottomSheet({
@@ -39,18 +40,36 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
40
final String paymentId;
41
final String paymentIdValue;
42
42
- TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
43
+ TextStyle _titleStyle(BuildContext ctx) =>
44
+ Theme
45
+ .of(ctx)
46
+ .textTheme
47
+ .bodyLarge!;
48
44
- TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
49
+ TextStyle _valueStyle(BuildContext ctx) =>
50
+ Theme
51
+ .of(ctx)
52
+ .textTheme
53
+ .titleMedium!
54
+ .copyWith(
55
fontSize: 18,
56
fontWeight: FontWeight.w600,
57
);
58
49
- TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
50
- color: Theme.of(ctx).colorScheme.onSurfaceVariant,
59
+ TextStyle _subStyle(BuildContext ctx) =>
60
+ Theme
61
+ .of(ctx)
62
+ .textTheme
63
+ .labelSmall!
64
+ .copyWith(
65
+ color: Theme
66
+ .of(ctx)
67
+ .colorScheme
68
+ .onSurfaceVariant,
69
);
70
53
- Widget _buildHeader(BuildContext ctx) => Column(
71
+ Widget _buildHeader(BuildContext ctx) =>
72
+ Column(
73
children: [
74
const SizedBox(height: 12),
75
Container(
@@ -58,7 +77,10 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
77
height: 5,
78
decoration: BoxDecoration(
79
borderRadius: BorderRadius.circular(4),
61
- color: Theme.of(ctx).colorScheme.onSurface,
80
+ color: Theme
81
+ .of(ctx)
82
+ .colorScheme
83
+ .onSurface,
84
),
85
),
86
const SizedBox(height: 20),
@@ -69,11 +91,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
91
const SizedBox(width: 6),
92
Text(
93
titleText,
72
- style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
73
- fontSize: 20,
74
- fontWeight: FontWeight.w600,
75
- decoration: TextDecoration.none,
76
- ),
94
+ style: Theme
95
+ .of(ctx)
96
+ .textTheme
97
+ .titleLarge!
98
+ .copyWith(
99
+ fontSize: 20,
100
+ fontWeight: FontWeight.w600,
101
+ decoration: TextDecoration.none,
102
+ ),
103
),
104
],
105
),
@@ -81,7 +107,8 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
107
],
108
);
109
84
- Widget _buildBody(BuildContext context) => Padding(
110
+ Widget _buildBody(BuildContext context) =>
111
+ Padding(
112
padding: const EdgeInsets.symmetric(horizontal: 8),
113
child: Column(
114
children: [
@@ -112,25 +139,49 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
139
itemSubTitleStyle: _subStyle(context),
140
imagePath: output.parsedAddress.profileImageUrl,
141
),
142
+ const SizedBox(height: 8),
143
+ _StandardTile(
144
+ itemTitle: paymentId + ': \n' + paymentIdValue,
145
+ titleStyle: _titleStyle(context).copyWith(
146
+ fontSize: 14
147
+ ),
148
+ itemValue: '',
149
+ itemValueStyle: _valueStyle(context),
150
+ copyButton: true,
151
+ copyButtonOnPressed: () async =>
152
+ await Clipboard.setData(ClipboardData(text: paymentIdValue)),
153
+ ),
154
+
155
Padding(
156
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
157
child: Column(
158
children: [
119
- Text(paymentId + ': ' + paymentIdValue,
120
- style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
159
const SizedBox(height: 18),
160
Image.asset('assets/images/envelope.png'),
161
const SizedBox(height: 18),
162
Text(
125
- S.of(context).cake_pay_card_email_delivered_message,
126
- style: Theme.of(context).textTheme.bodyLarge,
163
+ S
164
+ .of(context)
165
+ .cake_pay_card_email_delivered_message,
166
+ style: Theme
167
+ .of(context)
168
+ .textTheme
169
+ .bodyLarge,
170
textAlign: TextAlign.center,
171
),
172
const SizedBox(height: 24),
173
PrimaryButton(
131
- text: S.of(context).close,
132
- color: Theme.of(context).colorScheme.primary,
133
- textColor: Theme.of(context).colorScheme.onPrimaryContainer,
174
+ text: S
175
+ .of(context)
176
+ .close,
177
+ color: Theme
178
+ .of(context)
179
+ .colorScheme
180
+ .primary,
181
+ textColor: Theme
182
+ .of(context)
183
+ .colorScheme
184
+ .onPrimaryContainer,
185
onPressed: onClose,
186
),
187
],
@@ -142,12 +193,18 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
193
194
@override
195
Widget build(BuildContext context) {
145
- final maxHeight = MediaQuery.of(context).size.height * 0.9;
196
+ final maxHeight = MediaQuery
197
+ .of(context)
198
+ .size
199
+ .height * 0.9;
200
201
return ClipRRect(
202
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
203
child: Material(
150
- color: Theme.of(context).colorScheme.surface,
204
+ color: Theme
205
+ .of(context)
206
+ .colorScheme
207
+ .surface,
208
child: ConstrainedBox(
209
constraints: BoxConstraints(maxHeight: maxHeight),
210
child: SingleChildScrollView(
@@ -174,6 +231,8 @@ class _StandardTile extends StatelessWidget {
231
this.itemSubTitle,
232
this.itemSubTitleStyle,
233
this.imagePath,
234
+ this.copyButton = false,
235
+ this.copyButtonOnPressed,
236
});
237
238
final String itemTitle;
@@ -183,6 +242,8 @@ class _StandardTile extends StatelessWidget {
242
final String? itemSubTitle;
243
final TextStyle? itemSubTitleStyle;
244
final String? imagePath;
245
+ final bool copyButton;
246
+ final VoidCallback? copyButtonOnPressed;
247
248
@override
249
Widget build(BuildContext context) {
@@ -193,12 +254,17 @@ class _StandardTile extends StatelessWidget {
254
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
255
decoration: BoxDecoration(
256
borderRadius: BorderRadius.circular(10),
196
- color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)),
257
+ color: Theme
258
+ .of(context)
259
+ .colorScheme
260
+ .surfaceContainerLowest
261
+ .withAlpha(80)),
262
child: Row(
263
mainAxisAlignment: MainAxisAlignment.spaceBetween,
264
children: [
265
Expanded(
266
child: Row(
267
+ mainAxisAlignment: copyButton ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
268
children: [
269
if (imagePath != null)
270
Padding(
@@ -216,10 +282,19 @@ class _StandardTile extends StatelessWidget {
282
itemTitle,
283
style: titleStyle,
284
overflow: TextOverflow.ellipsis,
219
- maxLines: 1,
220
- softWrap: false,
285
+ maxLines: 2,
286
+ softWrap: true,
287
)),
288
),
289
+ SizedBox(width: 8),
290
+ if (copyButton)
291
+ RoundedIconButton(
292
+ icon: Icons.copy_all_outlined,
293
+ onPressed: copyButtonOnPressed ?? () {},
294
+ shape: RoundedRectangleBorder(
295
+ borderRadius: BorderRadius.circular(5),
296
+ ),
297
+ ),
298
],
299
),
300
),
@@ -237,3 +312,41 @@ class _StandardTile extends StatelessWidget {
312
);
313
}
314
}
315
+
316
+class RoundedIconButton extends StatelessWidget {
317
+ const RoundedIconButton({this.icon,
318
+ this.iconWidget,
319
+ required this.onPressed,
320
+ this.shape,
321
+ this.width,
322
+ this.height,
323
+ this.iconSize,
324
+ this.fillColor});
325
+
326
+ final IconData? icon;
327
+ final Widget? iconWidget;
328
+ final VoidCallback onPressed;
329
+ final ShapeBorder? shape;
330
+ final double? width;
331
+ final double? height;
332
+ final double? iconSize;
333
+ final Color? fillColor;
334
+
335
+ @override
336
+ Widget build(BuildContext context) {
337
+ final colorScheme = Theme
338
+ .of(context)
339
+ .colorScheme;
340
+ return RawMaterialButton(
341
+ onPressed: onPressed,
342
+ fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
343
+ elevation: 0,
344
+ constraints: BoxConstraints.tightFor(width: width ?? 30, height: height ?? 30),
345
+ padding: EdgeInsets.zero,
346
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
347
+ shape: shape ?? const CircleBorder(),
348
+ child: icon != null
349
+ ? Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface)
350
+ : iconWidget);
351
+ }
352
+}
lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
+73
-18
@@ -10,6 +10,7 @@ import 'package:cw_core/crypto_currency.dart';
10
import 'package:cw_core/pending_transaction.dart';
11
import 'package:cw_core/wallet_type.dart';
12
import 'package:flutter/material.dart';
13
+import 'package:flutter/services.dart';
14
import 'package:flutter_mobx/flutter_mobx.dart';
15
16
import 'base_bottom_sheet_widget.dart';
@@ -45,7 +46,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
46
_currentTheme = currentTheme,
47
super(
48
titleText: titleText,
48
- maxHeight: 900,
49
+ maxHeight: 900,
50
titleIconPath: titleIconPath,
51
currentTheme: currentTheme,
52
footerType: footerType,
@@ -115,6 +116,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
116
itemSubTitleTextStyle: itemSubTitleTextStyle,
117
tileBackgroundColor: tileBackgroundColor,
118
applyAddressFormatting: false,
119
+ copyButton: true,
120
)),
121
),
122
if (explanation != null)
@@ -307,20 +309,20 @@ class StandardInfoTile extends StatelessWidget {
309
}
310
311
class AddressTile extends StatelessWidget {
310
- const AddressTile({
311
- super.key,
312
- required this.itemTitle,
313
- required this.itemTitleTextStyle,
314
- required this.address,
315
- required this.itemSubTitleTextStyle,
316
- required this.tileBackgroundColor,
317
- required this.walletType,
318
- this.amountTextStyle,
319
- this.applyAddressFormatting = true,
320
- this.imagePath,
321
- this.amount,
322
- this.itemSubTitle,
323
- });
312
+ const AddressTile(
313
+ {super.key,
314
+ required this.itemTitle,
315
+ required this.itemTitleTextStyle,
316
+ required this.address,
317
+ required this.itemSubTitleTextStyle,
318
+ required this.tileBackgroundColor,
319
+ required this.walletType,
320
+ this.amountTextStyle,
321
+ this.applyAddressFormatting = true,
322
+ this.imagePath,
323
+ this.amount,
324
+ this.itemSubTitle,
325
+ this.copyButton = false});
326
327
final String itemTitle;
328
final TextStyle itemTitleTextStyle;
@@ -333,6 +335,7 @@ class AddressTile extends StatelessWidget {
335
final bool applyAddressFormatting;
336
final String? imagePath;
337
final String? itemSubTitle;
338
+ final bool copyButton;
339
340
@override
341
Widget build(BuildContext context) {
@@ -387,9 +390,24 @@ class AddressTile extends StatelessWidget {
390
fontWeight: FontWeight.w600,
391
decoration: TextDecoration.none,
392
))
390
- : Text(
391
- address,
392
- style: itemTitleTextStyle,
393
+ : Row(
394
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
395
+ children: [
396
+ Text(address,
397
+ style: copyButton
398
+ ? itemTitleTextStyle.copyWith(fontSize: 12)
399
+ : itemTitleTextStyle),
400
+ SizedBox(width: 8),
401
+ if (copyButton)
402
+ RoundedIconButton(
403
+ icon: Icons.copy_all_outlined,
404
+ onPressed: () async =>
405
+ await Clipboard.setData(ClipboardData(text: address)),
406
+ shape: RoundedRectangleBorder(
407
+ borderRadius: BorderRadius.circular(5),
408
+ ),
409
+ ),
410
+ ],
411
),
412
itemSubTitle == null
413
? Container()
@@ -490,3 +508,40 @@ class ExpansionAddressTile extends StatelessWidget {
508
);
509
}
510
}
511
+
512
+class RoundedIconButton extends StatelessWidget {
513
+ const RoundedIconButton(
514
+ {this.icon,
515
+ this.iconWidget,
516
+ required this.onPressed,
517
+ this.shape,
518
+ this.width,
519
+ this.height,
520
+ this.iconSize,
521
+ this.fillColor});
522
+
523
+ final IconData? icon;
524
+ final Widget? iconWidget;
525
+ final VoidCallback onPressed;
526
+ final ShapeBorder? shape;
527
+ final double? width;
528
+ final double? height;
529
+ final double? iconSize;
530
+ final Color? fillColor;
531
+
532
+ @override
533
+ Widget build(BuildContext context) {
534
+ final colorScheme = Theme.of(context).colorScheme;
535
+ return RawMaterialButton(
536
+ onPressed: onPressed,
537
+ fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
538
+ elevation: 0,
539
+ constraints: BoxConstraints.tightFor(width: width ?? 30, height: height ?? 30),
540
+ padding: EdgeInsets.zero,
541
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
542
+ shape: shape ?? const CircleBorder(),
543
+ child: icon != null
544
+ ? Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface)
545
+ : iconWidget);
546
+ }
547
+}