Skip to content
Draft
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
26 changes: 26 additions & 0 deletions lib/core/exchange/domain/entity/order.dart
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,29 @@ sealed class Order with _$Order {
}
}
}

extension FiatPaymentOrderDisplayX on FiatPaymentOrder {
/// The fiat amount the recipient gets, e.g. "125.00 CAD". The currency code
/// is used as returned by the server, without going through
/// [FiatCurrency.fromCode], which throws on codes the app doesn't know yet.
String get payoutAmountToDisplay =>
'${payoutAmount.toStringAsFixed(2)} $payoutCurrency';

/// The recipient as shown to the user. The server populates
/// [beneficiaryName] for most payout processors, but not all (SINPE can
/// legitimately have no owner name), so fall back to the label the user gave
/// the recipient and then to whichever identifier the payout method carries.
/// Null when the order carries nothing identifying at all.
String? get recipientToDisplay {
for (final candidate in [
beneficiaryName,
beneficiaryLabel,
beneficiaryAccountNumber,
beneficiaryETransferAddress,
lightningAddress,
]) {
if (candidate != null && candidate.isNotEmpty) return candidate;
}
return null;
}
}
10 changes: 9 additions & 1 deletion lib/features/pay/presentation/pay_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,9 @@ class PayBloc extends Bloc<PayEvent, PayState> {
if (state is PayPaymentState) {
emit((state as PayPaymentState).copyWith(isConfirmingPayment: false));
}
emit(payPaymentState.toSuccessState(payOrder: payPaymentState.payOrder));
// The order fetched after the broadcast, not the pre-broadcast one, so
// the success screens show the up-to-date payin status.
emit(payPaymentState.toSuccessState(payOrder: latestOrder));
} on PrepareLiquidSendException catch (e) {
emit(
payPaymentState.copyWith(
Expand Down Expand Up @@ -608,6 +610,12 @@ class PayBloc extends Bloc<PayEvent, PayState> {
// Convert Order to FiatPaymentOrder if needed
if (orderSummary is FiatPaymentOrder) {
emit(currentState.copyWith(payOrder: orderSummary));
} else {
log.severe(
error:
'Expected FiatPaymentOrder for order ${event.orderId} but received ${orderSummary.runtimeType}',
trace: StackTrace.current,
);
}
}
} catch (e) {
Expand Down
33 changes: 25 additions & 8 deletions lib/features/pay/ui/screens/pay_in_progress_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ class _PayInProgressScreenState extends State<PayInProgressScreen> {
_pollingTimer = null;
}

/// The copy follows the polled order: once the payin is confirmed onchain the
/// message says so, instead of asking the user to keep waiting for a
/// confirmation that already happened.
String _description(BuildContext context, FiatPaymentOrder order) {
final amount = order.payoutAmountToDisplay;
final recipient = order.recipientToDisplay ?? context.loc.payNotAvailable;

return order.isPayinCompleted
? context.loc.payPaymentPayinConfirmedDescriptionDetails(
amount,
recipient,
)
: context.loc.payPaymentInProgressDescriptionDetails(amount, recipient);
}

@override
Widget build(BuildContext context) {
final order = context.select(
Expand Down Expand Up @@ -134,15 +149,17 @@ class _PayInProgressScreenState extends State<PayInProgressScreen> {
context.loc.payPaymentInProgress,
style: context.font.titleLarge,
),
const Gap(10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
context.loc.payPaymentInProgressDescription,
style: context.font.bodyMedium,
textAlign: .center,
if (order != null) ...[
const Gap(10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
_description(context, order),
style: context.font.bodyMedium,
textAlign: .center,
),
),
),
],
],
),
),
Expand Down
22 changes: 14 additions & 8 deletions lib/features/pay/ui/screens/pay_success_screen.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:bb_mobile/core/exchange/domain/entity/order.dart';
import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:bb_mobile/core/utils/build_context_x.dart';
import 'package:bb_mobile/core/widgets/buttons/button.dart';
Expand Down Expand Up @@ -56,15 +57,20 @@ class PaySuccessScreen extends StatelessWidget {
),
const Gap(20),
Text(context.loc.payCompleted, style: context.font.titleLarge),
const Gap(10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
context.loc.payCompletedDescription,
style: context.font.bodyMedium,
textAlign: .center,
if (order != null) ...[
const Gap(10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
context.loc.payCompletedDescriptionDetails(
order.payoutAmountToDisplay,
order.recipientToDisplay ?? context.loc.payNotAvailable,
),
style: context.font.bodyMedium,
textAlign: .center,
),
),
),
],
],
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {
_broadcastOriginalTransactionUsecase;
final ProcessSwapUsecase _processSwapUsecase;

/// The load that populated this cubit, so [refresh] can re-run it whichever
/// init path the screen used. Orders only load once (they have no watcher),
/// which is why the screen needs a way to ask for fresh data.
Future<void> Function()? _reload;

StreamSubscription? _walletTransactionSubscription;
StreamSubscription? _swapSubscription;
StreamSubscription? _payjoinSubscription;
Expand All @@ -72,7 +77,29 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {
return super.close();
}

/// Re-runs the load that populated the details, for pull-to-refresh and for
/// retrying after a failed load.
Future<void> refresh() async {
final reload = _reload;
if (reload == null) return;

if (state.err != null || state.notFoundError != null) {
emit(state.copyWith(err: null, notFoundError: null));
}
await reload();
}

Future<void> initByWalletTxId(String txId, {required String walletId}) async {
// Keep the reload of whichever init the screen started with: an order that
// resolves to a wallet tx delegates here, and reloading the wallet tx also
// refetches the order.
_reload ??= () => _loadDetailsByWalletTxId(txId, walletId: walletId);

// An order-id entry whose order only later gains a transactionId re-enters
// here on every refresh, so drop the previous watcher instead of leaking a
// live one that keeps fetching per wallet-tx event.
await _walletTransactionSubscription?.cancel();

// Start monitoring the wallet transaction for updates.
_walletTransactionSubscription = _watchWalletTransactionByTxIdUsecase
.execute(txId: txId, walletId: walletId)
Expand Down Expand Up @@ -191,6 +218,10 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {

// Load the initial details of the swap.
await _loadDetailsBySwapId(swapId, walletId: walletId);

// Only when the swap didn't resolve to a wallet tx, which sets its own
// reload without re-subscribing the watchers.
_reload ??= () => _loadDetailsBySwapId(swapId, walletId: walletId);
}

Future<void> _loadDetailsBySwapId(
Expand Down Expand Up @@ -254,6 +285,8 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {

// Load the initial details of the payjoin.
await _loadDetailsByPayjoinId(payjoinId);

_reload ??= () => _loadDetailsByPayjoinId(payjoinId);
}

Future<void> _loadDetailsByPayjoinId(String payjoinId) async {
Expand Down Expand Up @@ -301,6 +334,10 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {

Future<void> initByOrderId(String orderId) async {
await _loadDetailsByOrderId(orderId);

// Only when the order didn't resolve to a wallet tx, which sets its own
// reload without re-subscribing the watchers.
_reload ??= () => _loadDetailsByOrderId(orderId);
}

Future<void> _loadDetailsByOrderId(String orderId) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ sealed class TransactionDetailsState with _$TransactionDetailsState {
}) = _TransactionDetailsState;
const TransactionDetailsState._();

bool get isLoading => transaction == null;
/// The load failed and there is nothing to show. Without this, a failed load
/// left the screen on loading skeletons forever.
bool get hasLoadError =>
transaction == null && (err != null || notFoundError != null);

bool get isLoading => transaction == null && !hasLoadError;

WalletTransaction? get walletTransaction => transaction?.walletTransaction;
Swap? get swap => transaction?.swap;
Expand Down
Loading