Skip to content
7 changes: 4 additions & 3 deletions bdk_demo/lib/core/router/app_router.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:go_router/go_router.dart';
import 'package:bdk_demo/features/transactions/transaction_detail_page.dart';
import 'package:bdk_demo/features/transactions/transactions_list_page.dart';
import 'package:bdk_demo/features/shared/widgets/placeholder_page.dart';
import 'package:bdk_demo/features/wallet_setup/active_wallets_page.dart';
import 'package:bdk_demo/features/wallet_setup/create_wallet_page.dart';
Expand Down Expand Up @@ -64,15 +66,14 @@ GoRouter createRouter() => GoRouter(
GoRoute(
path: AppRoutes.transactionHistory,
name: 'transactionHistory',
builder: (context, state) =>
const PlaceholderPage(title: 'Transaction History'),
builder: (context, state) => const TransactionsListPage(),
),
GoRoute(
path: AppRoutes.transactionDetail,
name: 'transactionDetail',
builder: (context, state) {
final txid = state.pathParameters['txid'] ?? '';
return PlaceholderPage(title: 'Transaction $txid');
return TransactionDetailPage(txid: txid);
},
),

Expand Down
4 changes: 2 additions & 2 deletions bdk_demo/lib/core/utils/formatters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ abstract final class Formatters {
return '$month ${dt.day} ${dt.year} $hour:$minute';
}

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

Expand Down
140 changes: 140 additions & 0 deletions bdk_demo/lib/features/shared/widgets/wallet_ui_helpers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';

import 'package:bdk_demo/core/theme/app_theme.dart';

class WalletStateCard extends StatelessWidget {
final IconData icon;
final String title;
final String message;
final Color? accentColor;
final bool showSpinner;
final bool centered;

const WalletStateCard({
super.key,
required this.icon,
required this.title,
required this.message,
this.accentColor,
this.showSpinner = false,
this.centered = false,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color = accentColor ?? theme.colorScheme.primary;

final card = Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
showSpinner
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(icon, color: color),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(message, style: theme.textTheme.bodyMedium),
],
),
),
],
),
),
);

if (!centered) return card;

return Center(
child: Padding(padding: const EdgeInsets.all(24), child: card),
);
}
}

class WalletDetailRow extends StatelessWidget {
final String label;
final String value;
final bool monospace;

const WalletDetailRow({
super.key,
required this.label,
required this.value,
this.monospace = false,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(170),
),
),
const SizedBox(height: 4),
Text(
value,
style: monospace
? AppTheme.monoStyle.copyWith(
fontSize: 13,
color: theme.colorScheme.onSurface,
)
: theme.textTheme.bodyLarge,
),
],
);
}
}

class WalletStatusChip extends StatelessWidget {
final String status;

const WalletStatusChip({super.key, required this.status});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isPending = status == 'pending';

return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
color: isPending
? theme.colorScheme.secondaryContainer
: theme.colorScheme.primaryContainer,
),
child: Text(
status,
style: theme.textTheme.labelMedium?.copyWith(
color: isPending
? theme.colorScheme.onSecondaryContainer
: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
);
}
}
Comment thread
j-kon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
class TxDetails {
import 'package:bdk_demo/core/utils/formatters.dart';

class DemoTxDetails {
final String txid;
final int sent;
final int received;
final int fee;
final double? feeRate;
final bool pending;
final int? blockHeight;
final DateTime? confirmationTime;

const TxDetails({
const DemoTxDetails({
required this.txid,
required this.sent,
required this.received,
this.fee = 0,
this.feeRate,
this.pending = true,
this.blockHeight,
this.confirmationTime,
});

int get netAmount => received - sent;

String get shortTxid => txid.length > 16
? '${txid.substring(0, 8)}...${txid.substring(txid.length - 8)}'
: txid;
String get shortTxid => Formatters.abbreviateTxid(txid);

String get statusLabel => pending ? 'pending' : 'confirmed';
}
163 changes: 163 additions & 0 deletions bdk_demo/lib/features/transactions/transaction_detail_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'package:bdk_demo/core/theme/app_theme.dart';
import 'package:bdk_demo/core/utils/formatters.dart';
import 'package:bdk_demo/features/shared/widgets/secondary_app_bar.dart';
import 'package:bdk_demo/features/shared/widgets/wallet_ui_helpers.dart';
import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart';
import 'package:bdk_demo/features/transactions/transactions_controller.dart';
import 'package:bdk_demo/models/currency_unit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class TransactionDetailPage extends ConsumerWidget {
final String txid;

const TransactionDetailPage({super.key, required this.txid});

String _formatAmount(DemoTxDetails transaction) {
final amount = transaction.netAmount;
final prefix = amount >= 0 ? '+' : '-';
final value = Formatters.formatBalance(amount.abs(), CurrencyUnit.satoshi);
return '$prefix$value';
}

String _formatTimestamp(DateTime timestamp) {
final unixSeconds = timestamp.millisecondsSinceEpoch ~/ 1000;
return Formatters.formatTimestamp(unixSeconds);
}

@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final transactionAsync = ref.watch(transactionDetailsProvider(txid));

return Scaffold(
appBar: const SecondaryAppBar(title: 'Transaction Detail'),
body: SafeArea(
child: transactionAsync.when(
loading: () => const WalletStateCard(
icon: Icons.hourglass_bottom,
title: 'Loading transaction',
message: 'Preparing placeholder transaction details...',
showSpinner: true,
centered: true,
),
error: (_, __) => WalletStateCard(
icon: Icons.error_outline,
title: 'Transaction unavailable',
message: 'The demo could not load placeholder transaction details.',
accentColor: theme.colorScheme.error,
centered: true,
),
data: (transaction) {
if (transaction == null) {
return WalletStateCard(
icon: Icons.search_off,
title: 'Transaction not found',
message:
'No placeholder transaction was found for this txid.\n\n$txid',
centered: true,
);
}

return ListView(
padding: const EdgeInsets.all(24),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
_formatAmount(transaction),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
WalletStatusChip(status: transaction.statusLabel),
],
),
const SizedBox(height: 8),
Text(
'Standalone transaction detail view for the selected placeholder transaction.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(170),
),
),
],
),
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Full txid',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(170),
),
),
const SizedBox(height: 8),
SelectableText(
transaction.txid,
style: AppTheme.monoStyle.copyWith(
fontSize: 13,
color: theme.colorScheme.onSurface,
),
),
],
),
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
WalletDetailRow(
label: 'Amount',
value: _formatAmount(transaction),
),
const SizedBox(height: 12),
WalletDetailRow(
label: 'Status',
value: transaction.statusLabel,
),
if (transaction.blockHeight != null) ...[
const SizedBox(height: 12),
WalletDetailRow(
label: 'Block height',
value: '${transaction.blockHeight}',
),
],
if (transaction.confirmationTime != null) ...[
const SizedBox(height: 12),
WalletDetailRow(
label: 'Timestamp',
value: _formatTimestamp(
transaction.confirmationTime!,
),
),
],
],
),
),
),
],
);
},
),
),
);
}
}
Loading
Loading