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 +}