Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:bdk_demo/core/utils/formatters.dart';

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

const DemoTxDetails({
const TransactionHistoryItem({
required this.txid,
required this.sent,
required this.received,
Expand Down
12 changes: 6 additions & 6 deletions bdk_demo/lib/features/transactions/transaction_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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/models/transaction_history_item.dart';
import 'package:bdk_demo/features/transactions/transactions_controller.dart';
import 'package:bdk_demo/models/currency_unit.dart';
import 'package:flutter/material.dart';
Expand All @@ -13,7 +13,7 @@ class TransactionDetailPage extends ConsumerWidget {

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

String _formatAmount(DemoTxDetails transaction) {
String _formatAmount(TransactionHistoryItem transaction) {
final amount = transaction.netAmount;
final prefix = amount >= 0 ? '+' : '-';
final value = Formatters.formatBalance(amount.abs(), CurrencyUnit.satoshi);
Expand All @@ -37,14 +37,14 @@ class TransactionDetailPage extends ConsumerWidget {
loading: () => const WalletStateCard(
icon: Icons.hourglass_bottom,
title: 'Loading transaction',
message: 'Preparing placeholder transaction details...',
message: 'Reading wallet transaction details...',
showSpinner: true,
centered: true,
),
error: (_, __) => WalletStateCard(
icon: Icons.error_outline,
title: 'Transaction unavailable',
message: 'The demo could not load placeholder transaction details.',
message: 'The wallet transaction details could not be loaded.',
accentColor: theme.colorScheme.error,
centered: true,
),
Expand All @@ -54,7 +54,7 @@ class TransactionDetailPage extends ConsumerWidget {
icon: Icons.search_off,
title: 'Transaction not found',
message:
'No placeholder transaction was found for this txid.\n\n$txid',
'No wallet transaction was found for this txid.\n\n$txid',
centered: true,
);
}
Expand Down Expand Up @@ -83,7 +83,7 @@ class TransactionDetailPage extends ConsumerWidget {
),
const SizedBox(height: 8),
Text(
'Standalone transaction detail view for the selected placeholder transaction.',
'Transaction detail for the selected wallet transaction.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(170),
),
Expand Down
52 changes: 52 additions & 0 deletions bdk_demo/lib/features/transactions/transaction_history_mapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart';

sealed class TransactionHistoryPosition {
const TransactionHistoryPosition();
}

class ConfirmedTransactionPosition extends TransactionHistoryPosition {
final int blockHeight;
final int confirmationTime;

const ConfirmedTransactionPosition({
required this.blockHeight,
required this.confirmationTime,
});
}

class UnconfirmedTransactionPosition extends TransactionHistoryPosition {
final int? timestamp;

const UnconfirmedTransactionPosition({this.timestamp});
}

class TransactionHistoryMapper {
const TransactionHistoryMapper._();

static TransactionHistoryItem fromWalletData({
required String txid,
required int sent,
required int received,
required TransactionHistoryPosition position,
}) {
return switch (position) {
ConfirmedTransactionPosition() => TransactionHistoryItem(
txid: txid,
sent: sent,
received: received,
pending: false,
blockHeight: position.blockHeight,
confirmationTime: DateTime.fromMillisecondsSinceEpoch(
position.confirmationTime * 1000,
isUtc: true,
),
),
UnconfirmedTransactionPosition() => TransactionHistoryItem(
txid: txid,
sent: sent,
received: received,
pending: true,
),
};
}
}
46 changes: 34 additions & 12 deletions bdk_demo/lib/features/transactions/transactions_controller.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart';
import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart';
import 'package:bdk_demo/features/transactions/transactions_repository.dart';
import 'package:bdk_demo/providers/wallet_providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

enum TransactionsLoadState { idle, loading, success, error }
enum TransactionsLoadState { idle, loading, success, error, noWallet }

class TransactionsState {
final TransactionsLoadState status;
final List<DemoTxDetails> transactions;
final List<TransactionHistoryItem> transactions;
final String statusMessage;
final String? errorMessage;

Expand All @@ -21,13 +22,12 @@ class TransactionsState {
: this(
status: TransactionsLoadState.idle,
transactions: const [],
statusMessage:
'Load the transaction demo to preview list and detail states.',
statusMessage: 'Load the active wallet transaction history.',
);

TransactionsState copyWith({
TransactionsLoadState? status,
List<DemoTxDetails>? transactions,
List<TransactionHistoryItem>? transactions,
String? statusMessage,
String? errorMessage,
}) {
Expand All @@ -46,20 +46,42 @@ final transactionsControllerProvider =
);

final transactionDetailsProvider =
FutureProvider.family<DemoTxDetails?, String>((ref, txid) {
FutureProvider.family<TransactionHistoryItem?, String>((ref, txid) {
final repository = ref.read(transactionsRepositoryProvider);
return repository.loadTransactionByTxid(txid);
});

class TransactionsController extends Notifier<TransactionsState> {
@override
TransactionsState build() => const TransactionsState.idle();
TransactionsState build() {
final hasWallet = ref.watch(hasActiveWalletProvider);
if (!hasWallet) {
return const TransactionsState(
status: TransactionsLoadState.noWallet,
transactions: [],
statusMessage:
'Create or load a wallet before viewing transaction history.',
);
}
return const TransactionsState.idle();
}

Future<void> loadTransactions() async {
final hasWallet = ref.read(hasActiveWalletProvider);
if (!hasWallet) {
state = const TransactionsState(
status: TransactionsLoadState.noWallet,
transactions: [],
statusMessage:
'Create or load a wallet before viewing transaction history.',
);
return;
}

state = state.copyWith(
status: TransactionsLoadState.loading,
transactions: const [],
statusMessage: 'Loading placeholder transactions...',
statusMessage: 'Loading transaction history...',
errorMessage: null,
);

Expand All @@ -72,15 +94,15 @@ class TransactionsController extends Notifier<TransactionsState> {
status: TransactionsLoadState.success,
transactions: transactions,
statusMessage: transactions.isEmpty
? 'Transaction demo loaded. No transactions yet.'
: 'Transaction demo loaded. Showing placeholder transaction rows.',
? 'Transaction history loaded. No transactions yet.'
: 'Transaction history loaded.',
errorMessage: null,
);
} catch (error) {
state = state.copyWith(
status: TransactionsLoadState.error,
transactions: const [],
statusMessage: 'The transaction demo could not be loaded.',
statusMessage: 'Transaction history could not be loaded.',
errorMessage: _readableError(error),
);
}
Expand Down
50 changes: 31 additions & 19 deletions bdk_demo/lib/features/transactions/transactions_list_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ 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/models/transaction_history_item.dart';
import 'package:bdk_demo/features/transactions/transactions_controller.dart';
import 'package:bdk_demo/models/currency_unit.dart';
import 'package:bdk_demo/providers/wallet_providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

class TransactionsListPage extends ConsumerWidget {
const TransactionsListPage({super.key});

void _openTransactionDetail(BuildContext context, DemoTxDetails transaction) {
void _openTransactionDetail(
BuildContext context,
TransactionHistoryItem transaction,
) {
context.pushNamed(
'transactionDetail',
pathParameters: {'txid': transaction.txid},
Expand All @@ -23,10 +27,12 @@ class TransactionsListPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final state = ref.watch(transactionsControllerProvider);
final hasWallet = ref.watch(hasActiveWalletProvider);
final isLoading = state.status == TransactionsLoadState.loading;
final canLoad = hasWallet && !isLoading;

return Scaffold(
appBar: const SecondaryAppBar(title: 'Transactions Demo'),
appBar: const SecondaryAppBar(title: 'Transaction History'),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(24),
Expand All @@ -51,25 +57,25 @@ class TransactionsListPage extends ConsumerWidget {
),
const SizedBox(height: 16),
Text(
'Transactions Demo',
'Transaction History',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Preview placeholder transaction list and detail states in a standalone transactions feature. This demo does not sync a real wallet or query the blockchain.',
'View transactions from the currently loaded wallet. Sync the wallet to refresh balance and history.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(180),
),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: isLoading
? null
: () => ref
onPressed: canLoad
? () => ref
.read(transactionsControllerProvider.notifier)
.loadTransactions(),
.loadTransactions()
: null,
icon: isLoading
? SizedBox(
width: 16,
Expand All @@ -83,8 +89,8 @@ class TransactionsListPage extends ConsumerWidget {
label: Text(
state.status == TransactionsLoadState.success ||
state.status == TransactionsLoadState.error
? 'Reload Transactions'
: 'Load Transactions Demo',
? 'Reload Transaction History'
: 'Load Transaction History',
),
),
],
Expand All @@ -94,7 +100,7 @@ class TransactionsListPage extends ConsumerWidget {
const SizedBox(height: 24),
const _SectionHeading(
title: 'Transactions',
subtitle: 'Placeholder transaction list and detail navigation',
subtitle: 'Active wallet transaction list and detail navigation',
),
const SizedBox(height: 12),
_TransactionsBody(state: state, onTap: _openTransactionDetail),
Expand All @@ -107,7 +113,8 @@ class TransactionsListPage extends ConsumerWidget {

class _TransactionsBody extends StatelessWidget {
final TransactionsState state;
final void Function(BuildContext context, DemoTxDetails transaction) onTap;
final void Function(BuildContext context, TransactionHistoryItem transaction)
onTap;

const _TransactionsBody({required this.state, required this.onTap});

Expand All @@ -116,20 +123,25 @@ class _TransactionsBody extends StatelessWidget {
final theme = Theme.of(context);

return switch (state.status) {
TransactionsLoadState.noWallet => const WalletStateCard(
icon: Icons.account_balance_wallet_outlined,
title: 'No active wallet',
message: 'Create or load a wallet before viewing transaction history.',
),
TransactionsLoadState.idle => WalletStateCard(
icon: Icons.info_outline,
title: 'Transactions not loaded yet',
title: 'Transaction history not loaded yet',
message: state.statusMessage,
),
TransactionsLoadState.loading => const WalletStateCard(
icon: Icons.hourglass_bottom,
title: 'Loading placeholder transactions...',
message: 'Preparing scaffolded transaction rows.',
title: 'Loading transaction history...',
message: 'Reading wallet transactions.',
showSpinner: true,
),
TransactionsLoadState.error => WalletStateCard(
icon: Icons.error_outline,
title: 'Transaction demo failed',
title: 'Transaction history failed',
message: state.errorMessage ?? state.statusMessage,
accentColor: theme.colorScheme.error,
),
Expand All @@ -139,7 +151,7 @@ class _TransactionsBody extends StatelessWidget {
icon: Icons.history_toggle_off,
title: 'No transactions yet',
message:
'The transaction demo loaded successfully, but no placeholder transactions are configured yet.',
'The active wallet has no transactions yet. Sync the wallet or receive funds to populate history.',
)
: Card(
child: Padding(
Expand Down Expand Up @@ -199,7 +211,7 @@ class _SectionHeading extends StatelessWidget {
}

class _TransactionRow extends StatelessWidget {
final DemoTxDetails transaction;
final TransactionHistoryItem transaction;
final VoidCallback onTap;

const _TransactionRow({required this.transaction, required this.onTap});
Expand Down
Loading
Loading