Skip to content

Commit 74854b9

Browse files
committed
feat(AddressViewer): tap to view in details and copy, press to copy.
Address length adapt to the screen size
1 parent 58da22f commit 74854b9

2 files changed

Lines changed: 193 additions & 75 deletions

File tree

lib/core/transactions/ui/transaction_screen.dart

Lines changed: 24 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import 'package:bb_mobile/core/transactions/domain/error/transaction_error.dart'
55
import 'package:bb_mobile/core/transactions/presentation/transaction_cubit.dart';
66
import 'package:bb_mobile/core/transactions/presentation/transaction_state.dart';
77
import 'package:bb_mobile/core/utils/build_context_x.dart';
8-
import 'package:bb_mobile/core/utils/string_formatting.dart';
8+
import 'package:bb_mobile/core/widgets/address_viewer.dart';
99
import 'package:bb_mobile/core/widgets/loading/fading_linear_progress.dart';
1010
import 'package:bb_mobile/core/widgets/text/text.dart';
1111
import 'package:flutter/material.dart';
12-
import 'package:flutter/services.dart';
1312
import 'package:flutter_bloc/flutter_bloc.dart';
1413
import 'package:gap/gap.dart';
1514

@@ -396,28 +395,13 @@ class _SummarySection extends StatelessWidget {
396395
if (toLabel != null && recipientOutputs.length == 1) {
397396
return _InfoRow(
398397
label: context.loc.coreScreensToLabel,
399-
child: Row(
400-
mainAxisAlignment: MainAxisAlignment.end,
401-
children: [
402-
Expanded(
403-
child: BBText(
404-
toLabel!,
405-
style: context.font.bodyLarge,
406-
color: context.appColors.secondary,
407-
textAlign: TextAlign.end,
408-
maxLines: 5,
409-
),
410-
),
411-
const Gap(4),
412-
GestureDetector(
413-
onTap: () => Clipboard.setData(ClipboardData(text: toLabel!)),
414-
child: Icon(
415-
Icons.copy,
416-
color: context.appColors.primary,
417-
size: 16,
418-
),
419-
),
420-
],
398+
child: Align(
399+
alignment: Alignment.centerRight,
400+
child: AddressViewer(
401+
toLabel!,
402+
style: context.font.bodyLarge,
403+
color: context.appColors.secondary,
404+
),
421405
),
422406
);
423407
}
@@ -459,37 +443,20 @@ class _InputAddressRow extends StatelessWidget {
459443

460444
@override
461445
Widget build(BuildContext context) {
462-
final displayAddress =
463-
input.address ?? StringFormatting.truncateMiddle(input.previousTxId);
464-
final copyText = input.address ?? input.previousTxId;
446+
final fullAddress = input.address ?? input.previousTxId;
465447

466448
return Padding(
467449
padding: const EdgeInsets.only(bottom: 6),
468450
child: Column(
469451
crossAxisAlignment: CrossAxisAlignment.end,
470452
children: [
471-
Row(
472-
mainAxisAlignment: MainAxisAlignment.end,
473-
children: [
474-
Expanded(
475-
child: BBText(
476-
displayAddress,
477-
style: context.font.bodySmall,
478-
color: context.appColors.secondary,
479-
textAlign: TextAlign.end,
480-
maxLines: 3,
481-
),
482-
),
483-
const Gap(4),
484-
GestureDetector(
485-
onTap: () => Clipboard.setData(ClipboardData(text: copyText)),
486-
child: Icon(
487-
Icons.copy,
488-
color: context.appColors.primary,
489-
size: 14,
490-
),
491-
),
492-
],
453+
Align(
454+
alignment: Alignment.centerRight,
455+
child: AddressViewer(
456+
fullAddress,
457+
style: context.font.bodySmall,
458+
color: context.appColors.secondary,
459+
),
493460
),
494461
BBText(
495462
'${_formatSats(input.valueSat)} sats',
@@ -511,38 +478,20 @@ class _OutputAddressRow extends StatelessWidget {
511478

512479
@override
513480
Widget build(BuildContext context) {
514-
final address = output.address;
515-
final displayAddress =
516-
address ?? StringFormatting.truncateMiddle(output.scriptPubKeyHex);
517-
final copyText = address ?? output.scriptPubKeyHex;
481+
final fullAddress = output.address ?? output.scriptPubKeyHex;
518482

519483
return Padding(
520484
padding: const EdgeInsets.only(bottom: 6),
521485
child: Column(
522486
crossAxisAlignment: CrossAxisAlignment.end,
523487
children: [
524-
Row(
525-
mainAxisAlignment: MainAxisAlignment.end,
526-
children: [
527-
Expanded(
528-
child: BBText(
529-
displayAddress,
530-
style: context.font.bodySmall,
531-
color: context.appColors.secondary,
532-
textAlign: TextAlign.end,
533-
maxLines: 3,
534-
),
535-
),
536-
const Gap(4),
537-
GestureDetector(
538-
onTap: () => Clipboard.setData(ClipboardData(text: copyText)),
539-
child: Icon(
540-
Icons.copy,
541-
color: context.appColors.primary,
542-
size: 14,
543-
),
544-
),
545-
],
488+
Align(
489+
alignment: Alignment.centerRight,
490+
child: AddressViewer(
491+
fullAddress,
492+
style: context.font.bodySmall,
493+
color: context.appColors.secondary,
494+
),
546495
),
547496
BBText(
548497
'${_formatSats(output.valueSat)} sats',
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import 'package:bb_mobile/core/themes/app_theme.dart';
2+
import 'package:bb_mobile/core/widgets/dialog/blurred_dialog.dart';
3+
import 'package:bb_mobile/core/widgets/snackbar_utils.dart';
4+
import 'package:bb_mobile/core/widgets/text/text.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/services.dart';
7+
import 'package:gap/gap.dart';
8+
9+
/// Displays an address that adapts to the available width.
10+
///
11+
/// Shows as many characters as fit on a single line (minimum 5 head + 5 tail
12+
/// with "…" in the middle). If the full address fits, no truncation is applied.
13+
///
14+
/// - **Tap** opens a bottom sheet showing the full address split into
15+
/// groups of 4 for easy visual verification.
16+
/// - **Long press** copies the full address to the clipboard.
17+
class AddressViewer extends StatelessWidget {
18+
const AddressViewer(this.address, {super.key, this.style, this.color});
19+
20+
final String address;
21+
final TextStyle? style;
22+
final Color? color;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
final textColor = color ?? context.appColors.secondary;
27+
final textStyle = style ?? context.font.bodyLarge;
28+
29+
return GestureDetector(
30+
onTap: () => _showAddressSheet(context),
31+
onLongPress: () {
32+
Clipboard.setData(ClipboardData(text: address));
33+
SnackBarUtils.showCopiedSnackBar(context);
34+
},
35+
child: LayoutBuilder(
36+
builder: (context, constraints) {
37+
final truncated = _fitToWidth(
38+
address,
39+
textStyle?.copyWith(color: textColor) ?? const TextStyle(),
40+
constraints.maxWidth,
41+
);
42+
return BBText(
43+
truncated,
44+
style: textStyle,
45+
color: textColor,
46+
maxLines: 1,
47+
overflow: TextOverflow.clip,
48+
);
49+
},
50+
),
51+
);
52+
}
53+
54+
/// Returns the address truncated to fit [maxWidth], or the full address
55+
/// if it fits. Minimum truncation keeps 5 chars on each side.
56+
static String _fitToWidth(String address, TextStyle style, double maxWidth) {
57+
if (_measure(address, style) <= maxWidth) return address;
58+
59+
const separator = '…';
60+
const minChars = 5;
61+
62+
// Start from the maximum possible and shrink until it fits
63+
final maxSide = (address.length - 1) ~/ 2;
64+
for (int n = maxSide; n >= minChars; n--) {
65+
final truncated =
66+
'${address.substring(0, n)}$separator${address.substring(address.length - n)}';
67+
if (_measure(truncated, style) <= maxWidth) return truncated;
68+
}
69+
70+
// Absolute fallback
71+
return '${address.substring(0, minChars)}$separator${address.substring(address.length - minChars)}';
72+
}
73+
74+
static double _measure(String text, TextStyle style) {
75+
final painter = TextPainter(
76+
text: TextSpan(text: text, style: style),
77+
maxLines: 1,
78+
textDirection: TextDirection.ltr,
79+
)..layout();
80+
return painter.width;
81+
}
82+
83+
void _showAddressSheet(BuildContext context) {
84+
BlurredDialog.show(
85+
context: context,
86+
builder: (dialogContext) =>
87+
_AddressDetailSheet(address: address, dialogContext: dialogContext),
88+
);
89+
}
90+
}
91+
92+
class _AddressDetailSheet extends StatelessWidget {
93+
const _AddressDetailSheet({
94+
required this.address,
95+
required this.dialogContext,
96+
});
97+
98+
final String address;
99+
final BuildContext dialogContext;
100+
101+
static const int _groupSize = 4;
102+
103+
List<String> get _groups {
104+
final groups = <String>[];
105+
for (int i = 0; i < address.length; i += _groupSize) {
106+
final end = i + _groupSize > address.length
107+
? address.length
108+
: i + _groupSize;
109+
groups.add(address.substring(i, end));
110+
}
111+
return groups;
112+
}
113+
114+
@override
115+
Widget build(BuildContext context) {
116+
return Padding(
117+
padding: const EdgeInsets.fromLTRB(24, 24, 24, 40),
118+
child: Column(
119+
mainAxisSize: MainAxisSize.min,
120+
children: [
121+
BBText(
122+
'Address',
123+
style: context.font.titleSmall,
124+
color: context.appColors.onSurface,
125+
),
126+
const Gap(16),
127+
Wrap(
128+
spacing: 8,
129+
runSpacing: 6,
130+
alignment: WrapAlignment.center,
131+
children: [
132+
for (final group in _groups)
133+
Text(
134+
group,
135+
style: context.font.bodyLarge?.copyWith(
136+
color: context.appColors.secondary,
137+
fontFeatures: [const FontFeature.tabularFigures()],
138+
letterSpacing: 1.2,
139+
),
140+
),
141+
],
142+
),
143+
144+
const Gap(24),
145+
GestureDetector(
146+
behavior: HitTestBehavior.opaque,
147+
onTap: () {
148+
Clipboard.setData(ClipboardData(text: address));
149+
Navigator.of(dialogContext).pop();
150+
SnackBarUtils.showCopiedSnackBar(dialogContext);
151+
},
152+
child: Row(
153+
mainAxisAlignment: MainAxisAlignment.center,
154+
children: [
155+
Icon(Icons.copy, size: 14, color: context.appColors.primary),
156+
const Gap(4),
157+
BBText(
158+
'Tap to copy',
159+
style: context.font.bodySmall,
160+
color: context.appColors.secondary,
161+
),
162+
],
163+
),
164+
),
165+
],
166+
),
167+
);
168+
}
169+
}

0 commit comments

Comments
 (0)