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
18 changes: 11 additions & 7 deletions bdk_demo/lib/features/transactions/transaction_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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';

Expand All @@ -13,7 +14,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 @@ -28,7 +29,10 @@ class TransactionDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final transactionAsync = ref.watch(transactionDetailsProvider(txid));
final activeWalletId = ref.watch(activeWalletIdProvider);
final transactionAsync = ref.watch(
transactionDetailsProvider((walletId: activeWalletId, txid: txid)),
);

return Scaffold(
appBar: const SecondaryAppBar(title: 'Transaction Detail'),
Expand All @@ -37,14 +41,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 +58,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 +87,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,
),
};
}
}
155 changes: 120 additions & 35 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 @@ -18,71 +19,155 @@ class TransactionsState {
});

const TransactionsState.idle()
: this(
status: TransactionsLoadState.idle,
transactions: const [],
statusMessage:
'Load the transaction demo to preview list and detail states.',
);
: status = TransactionsLoadState.idle,
transactions = const [],
statusMessage = 'Ready to load transactions.',
errorMessage = null;

static const _unset = Object();

TransactionsState copyWith({
TransactionsLoadState? status,
List<DemoTxDetails>? transactions,
List<TransactionHistoryItem>? transactions,
String? statusMessage,
String? errorMessage,
Object? errorMessage = _unset,
}) {
return TransactionsState(
status: status ?? this.status,
transactions: transactions ?? this.transactions,
statusMessage: statusMessage ?? this.statusMessage,
errorMessage: errorMessage,
errorMessage: identical(errorMessage, _unset)
? this.errorMessage
: errorMessage as String?,
);
}
}

final transactionsControllerProvider =
NotifierProvider<TransactionsController, TransactionsState>(
final transactionsControllerProvider = NotifierProvider.autoDispose
.family<TransactionsController, TransactionsState, String?>(
TransactionsController.new,
);

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

class TransactionsController extends Notifier<TransactionsState> {
TransactionsController(this.walletId);

final String? walletId;
Future<void>? _inFlightLoad;
bool _hasPendingRefresh = false;
bool _pendingIsBackground = true;

@override
TransactionsState build() => const TransactionsState.idle();

Future<void> loadTransactions() async {
state = state.copyWith(
status: TransactionsLoadState.loading,
transactions: const [],
statusMessage: 'Loading placeholder transactions...',
errorMessage: null,
);
TransactionsState build() {
if (walletId == null) {
return const TransactionsState(
status: TransactionsLoadState.noWallet,
transactions: [],
statusMessage:
'Create or load a wallet before viewing transaction history.',
);
}

ref.listen(activeWalletProvider, (previous, next) {
if (next != null) {
final isSuccess = state.status == TransactionsLoadState.success;
loadTransactions(isBackgroundRefresh: isSuccess);
}
});

Future.microtask(() => loadTransactions());

return const TransactionsState.idle();
}

Future<void> loadTransactions({bool isBackgroundRefresh = false}) async {
if (walletId == null) {
state = const TransactionsState(
status: TransactionsLoadState.noWallet,
transactions: [],
statusMessage:
'Create or load a wallet before viewing transaction history.',
);
return;
}

if (_inFlightLoad != null) {
_hasPendingRefresh = true;
if (!isBackgroundRefresh) {
_pendingIsBackground = false;
}
return _inFlightLoad;
}

_inFlightLoad = _performLoad(isBackgroundRefresh: isBackgroundRefresh);
try {
await _inFlightLoad;
} finally {
_inFlightLoad = null;
if (ref.mounted && _hasPendingRefresh) {
final isBg = _pendingIsBackground;
_hasPendingRefresh = false;
_pendingIsBackground = true;
await loadTransactions(isBackgroundRefresh: isBg);
}
}
}

Future<void> _performLoad({required bool isBackgroundRefresh}) async {
if (!isBackgroundRefresh) {
state = state.copyWith(
status: TransactionsLoadState.loading,
transactions: const [],
statusMessage: 'Loading transaction history...',
errorMessage: null,
);
}

try {
final transactions = await ref
.read(transactionsRepositoryProvider)
.loadTransactions();

if (!ref.mounted) {
return;
}

state = state.copyWith(
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.',
errorMessage: _readableError(error),
);
if (!ref.mounted) {
return;
}

if (isBackgroundRefresh &&
state.status == TransactionsLoadState.success) {
state = state.copyWith(
status: TransactionsLoadState.success,
transactions: state.transactions,
errorMessage: _readableError(error),
);
} else {
state = state.copyWith(
status: TransactionsLoadState.error,
transactions: const [],
statusMessage: 'Transaction history could not be loaded.',
errorMessage: _readableError(error),
);
}
}
}

Expand Down
Loading
Loading