Skip to content

Commit cd8e94a

Browse files
authored
feat(example): scaffold transaction presentation in bdk_demo active wallet flow
1 parent 519f423 commit cd8e94a

16 files changed

Lines changed: 1029 additions & 15 deletions

bdk_demo/lib/core/router/app_router.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:go_router/go_router.dart';
2+
import 'package:bdk_demo/features/transactions/transaction_detail_page.dart';
3+
import 'package:bdk_demo/features/transactions/transactions_list_page.dart';
24
import 'package:bdk_demo/features/shared/widgets/placeholder_page.dart';
35
import 'package:bdk_demo/features/wallet_setup/active_wallets_page.dart';
46
import 'package:bdk_demo/features/wallet_setup/create_wallet_page.dart';
@@ -64,15 +66,14 @@ GoRouter createRouter() => GoRouter(
6466
GoRoute(
6567
path: AppRoutes.transactionHistory,
6668
name: 'transactionHistory',
67-
builder: (context, state) =>
68-
const PlaceholderPage(title: 'Transaction History'),
69+
builder: (context, state) => const TransactionsListPage(),
6970
),
7071
GoRoute(
7172
path: AppRoutes.transactionDetail,
7273
name: 'transactionDetail',
7374
builder: (context, state) {
7475
final txid = state.pathParameters['txid'] ?? '';
75-
return PlaceholderPage(title: 'Transaction $txid');
76+
return TransactionDetailPage(txid: txid);
7677
},
7778
),
7879

bdk_demo/lib/core/utils/formatters.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ abstract final class Formatters {
3232
return '$month ${dt.day} ${dt.year} $hour:$minute';
3333
}
3434

35-
static String abbreviateTxid(String txid) => txid.length > 16
36-
? '${txid.substring(0, 8)}...${txid.substring(txid.length - 8)}'
35+
static String abbreviateTxid(String txid) => txid.length > 10
36+
? '${txid.substring(0, 6)}...${txid.substring(txid.length - 4)}'
3737
: txid;
3838
}
3939

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'package:bdk_demo/core/theme/app_theme.dart';
4+
5+
class WalletStateCard extends StatelessWidget {
6+
final IconData icon;
7+
final String title;
8+
final String message;
9+
final Color? accentColor;
10+
final bool showSpinner;
11+
final bool centered;
12+
13+
const WalletStateCard({
14+
super.key,
15+
required this.icon,
16+
required this.title,
17+
required this.message,
18+
this.accentColor,
19+
this.showSpinner = false,
20+
this.centered = false,
21+
});
22+
23+
@override
24+
Widget build(BuildContext context) {
25+
final theme = Theme.of(context);
26+
final color = accentColor ?? theme.colorScheme.primary;
27+
28+
final card = Card(
29+
child: Padding(
30+
padding: const EdgeInsets.all(20),
31+
child: Row(
32+
crossAxisAlignment: CrossAxisAlignment.start,
33+
children: [
34+
showSpinner
35+
? const SizedBox(
36+
width: 20,
37+
height: 20,
38+
child: CircularProgressIndicator(strokeWidth: 2),
39+
)
40+
: Icon(icon, color: color),
41+
const SizedBox(width: 12),
42+
Expanded(
43+
child: Column(
44+
crossAxisAlignment: CrossAxisAlignment.start,
45+
mainAxisSize: MainAxisSize.min,
46+
children: [
47+
Text(
48+
title,
49+
style: theme.textTheme.titleMedium?.copyWith(
50+
fontWeight: FontWeight.w600,
51+
),
52+
),
53+
const SizedBox(height: 6),
54+
Text(message, style: theme.textTheme.bodyMedium),
55+
],
56+
),
57+
),
58+
],
59+
),
60+
),
61+
);
62+
63+
if (!centered) return card;
64+
65+
return Center(
66+
child: Padding(padding: const EdgeInsets.all(24), child: card),
67+
);
68+
}
69+
}
70+
71+
class WalletDetailRow extends StatelessWidget {
72+
final String label;
73+
final String value;
74+
final bool monospace;
75+
76+
const WalletDetailRow({
77+
super.key,
78+
required this.label,
79+
required this.value,
80+
this.monospace = false,
81+
});
82+
83+
@override
84+
Widget build(BuildContext context) {
85+
final theme = Theme.of(context);
86+
87+
return Column(
88+
crossAxisAlignment: CrossAxisAlignment.start,
89+
children: [
90+
Text(
91+
label,
92+
style: theme.textTheme.labelLarge?.copyWith(
93+
color: theme.colorScheme.onSurface.withAlpha(170),
94+
),
95+
),
96+
const SizedBox(height: 4),
97+
Text(
98+
value,
99+
style: monospace
100+
? AppTheme.monoStyle.copyWith(
101+
fontSize: 13,
102+
color: theme.colorScheme.onSurface,
103+
)
104+
: theme.textTheme.bodyLarge,
105+
),
106+
],
107+
);
108+
}
109+
}
110+
111+
class WalletStatusChip extends StatelessWidget {
112+
final String status;
113+
114+
const WalletStatusChip({super.key, required this.status});
115+
116+
@override
117+
Widget build(BuildContext context) {
118+
final theme = Theme.of(context);
119+
final isPending = status == 'pending';
120+
121+
return Container(
122+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
123+
decoration: BoxDecoration(
124+
borderRadius: BorderRadius.circular(999),
125+
color: isPending
126+
? theme.colorScheme.secondaryContainer
127+
: theme.colorScheme.primaryContainer,
128+
),
129+
child: Text(
130+
status,
131+
style: theme.textTheme.labelMedium?.copyWith(
132+
color: isPending
133+
? theme.colorScheme.onSecondaryContainer
134+
: theme.colorScheme.onPrimaryContainer,
135+
fontWeight: FontWeight.w600,
136+
),
137+
),
138+
);
139+
}
140+
}

bdk_demo/lib/models/tx_details.dart renamed to bdk_demo/lib/features/transactions/models/demo_tx_details.dart

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
class TxDetails {
1+
import 'package:bdk_demo/core/utils/formatters.dart';
2+
3+
class DemoTxDetails {
24
final String txid;
35
final int sent;
46
final int received;
5-
final int fee;
6-
final double? feeRate;
77
final bool pending;
88
final int? blockHeight;
99
final DateTime? confirmationTime;
1010

11-
const TxDetails({
11+
const DemoTxDetails({
1212
required this.txid,
1313
required this.sent,
1414
required this.received,
15-
this.fee = 0,
16-
this.feeRate,
1715
this.pending = true,
1816
this.blockHeight,
1917
this.confirmationTime,
2018
});
2119

2220
int get netAmount => received - sent;
2321

24-
String get shortTxid => txid.length > 16
25-
? '${txid.substring(0, 8)}...${txid.substring(txid.length - 8)}'
26-
: txid;
22+
String get shortTxid => Formatters.abbreviateTxid(txid);
23+
24+
String get statusLabel => pending ? 'pending' : 'confirmed';
2725
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import 'package:bdk_demo/core/theme/app_theme.dart';
2+
import 'package:bdk_demo/core/utils/formatters.dart';
3+
import 'package:bdk_demo/features/shared/widgets/secondary_app_bar.dart';
4+
import 'package:bdk_demo/features/shared/widgets/wallet_ui_helpers.dart';
5+
import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart';
6+
import 'package:bdk_demo/features/transactions/transactions_controller.dart';
7+
import 'package:bdk_demo/models/currency_unit.dart';
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter_riverpod/flutter_riverpod.dart';
10+
11+
class TransactionDetailPage extends ConsumerWidget {
12+
final String txid;
13+
14+
const TransactionDetailPage({super.key, required this.txid});
15+
16+
String _formatAmount(DemoTxDetails transaction) {
17+
final amount = transaction.netAmount;
18+
final prefix = amount >= 0 ? '+' : '-';
19+
final value = Formatters.formatBalance(amount.abs(), CurrencyUnit.satoshi);
20+
return '$prefix$value';
21+
}
22+
23+
String _formatTimestamp(DateTime timestamp) {
24+
final unixSeconds = timestamp.millisecondsSinceEpoch ~/ 1000;
25+
return Formatters.formatTimestamp(unixSeconds);
26+
}
27+
28+
@override
29+
Widget build(BuildContext context, WidgetRef ref) {
30+
final theme = Theme.of(context);
31+
final transactionAsync = ref.watch(transactionDetailsProvider(txid));
32+
33+
return Scaffold(
34+
appBar: const SecondaryAppBar(title: 'Transaction Detail'),
35+
body: SafeArea(
36+
child: transactionAsync.when(
37+
loading: () => const WalletStateCard(
38+
icon: Icons.hourglass_bottom,
39+
title: 'Loading transaction',
40+
message: 'Preparing placeholder transaction details...',
41+
showSpinner: true,
42+
centered: true,
43+
),
44+
error: (_, __) => WalletStateCard(
45+
icon: Icons.error_outline,
46+
title: 'Transaction unavailable',
47+
message: 'The demo could not load placeholder transaction details.',
48+
accentColor: theme.colorScheme.error,
49+
centered: true,
50+
),
51+
data: (transaction) {
52+
if (transaction == null) {
53+
return WalletStateCard(
54+
icon: Icons.search_off,
55+
title: 'Transaction not found',
56+
message:
57+
'No placeholder transaction was found for this txid.\n\n$txid',
58+
centered: true,
59+
);
60+
}
61+
62+
return ListView(
63+
padding: const EdgeInsets.all(24),
64+
children: [
65+
Card(
66+
child: Padding(
67+
padding: const EdgeInsets.all(20),
68+
child: Column(
69+
crossAxisAlignment: CrossAxisAlignment.start,
70+
children: [
71+
Row(
72+
children: [
73+
Expanded(
74+
child: Text(
75+
_formatAmount(transaction),
76+
style: theme.textTheme.headlineSmall?.copyWith(
77+
fontWeight: FontWeight.w700,
78+
),
79+
),
80+
),
81+
WalletStatusChip(status: transaction.statusLabel),
82+
],
83+
),
84+
const SizedBox(height: 8),
85+
Text(
86+
'Standalone transaction detail view for the selected placeholder transaction.',
87+
style: theme.textTheme.bodyMedium?.copyWith(
88+
color: theme.colorScheme.onSurface.withAlpha(170),
89+
),
90+
),
91+
],
92+
),
93+
),
94+
),
95+
const SizedBox(height: 24),
96+
Card(
97+
child: Padding(
98+
padding: const EdgeInsets.all(20),
99+
child: Column(
100+
crossAxisAlignment: CrossAxisAlignment.start,
101+
children: [
102+
Text(
103+
'Full txid',
104+
style: theme.textTheme.labelLarge?.copyWith(
105+
color: theme.colorScheme.onSurface.withAlpha(170),
106+
),
107+
),
108+
const SizedBox(height: 8),
109+
SelectableText(
110+
transaction.txid,
111+
style: AppTheme.monoStyle.copyWith(
112+
fontSize: 13,
113+
color: theme.colorScheme.onSurface,
114+
),
115+
),
116+
],
117+
),
118+
),
119+
),
120+
const SizedBox(height: 24),
121+
Card(
122+
child: Padding(
123+
padding: const EdgeInsets.all(20),
124+
child: Column(
125+
crossAxisAlignment: CrossAxisAlignment.start,
126+
children: [
127+
WalletDetailRow(
128+
label: 'Amount',
129+
value: _formatAmount(transaction),
130+
),
131+
const SizedBox(height: 12),
132+
WalletDetailRow(
133+
label: 'Status',
134+
value: transaction.statusLabel,
135+
),
136+
if (transaction.blockHeight != null) ...[
137+
const SizedBox(height: 12),
138+
WalletDetailRow(
139+
label: 'Block height',
140+
value: '${transaction.blockHeight}',
141+
),
142+
],
143+
if (transaction.confirmationTime != null) ...[
144+
const SizedBox(height: 12),
145+
WalletDetailRow(
146+
label: 'Timestamp',
147+
value: _formatTimestamp(
148+
transaction.confirmationTime!,
149+
),
150+
),
151+
],
152+
],
153+
),
154+
),
155+
),
156+
],
157+
);
158+
},
159+
),
160+
),
161+
);
162+
}
163+
}

0 commit comments

Comments
 (0)