fix(shopinbit): don't pop the whole nav stack when PAY NOW has no address
What changed, and why it matters
This commit fixes a bug in Stack Wallet's ShopInBit payment screen. Previously, if the user tapped 'PAY NOW' before the payment address had loaded, the app would abruptly close the entire navigation stack (effectively kicking the user out of the checkout). Now it shows a warning message, attempts to recover the missing payment details from the server, and continues polling instead of popping screens.
Treat as a routine bug/UX fix. Review the fetchShopInBitPaymentInfo and _applyPaymentInfo implementations to ensure recovered payment details are validated before use, and verify that the warning message is surfaced consistently on both mobile and desktop.
Security signals we found
UI state recovery for missing payment address
Removal of unconditional Navigator.popUntil/pop that could discard in-flight checkout state
Addition of server-side payment-info recovery via GET/PUT before polling
No input validation, crypto, or authentication changes visible in diff
Evidence from the diff
In lib/pages/shopinbit/shopinbit_payment_view.dart, the initState logic now checks whether any payment address is empty and calls a new _recoverPaymentInfo() helper to fetch and apply payment info before starting the polling timer. The ‘PAY NOW’ handler no longer blindly pops the navigation stack; instead it displays a warning flushbar and, if the order isn’t terminal, resumes polling. This addresses a UX/state-recovery issue rather than a cryptographic or network-trust vulnerability.
Changed components
lib/pages/shopinbit/shopinbit_payment_view.dartShopInBit payment flowPAY NOW button handlerPayment-info polling/recovery logicInspect captured patch +28 / −10
diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart
index 589e6f2..6ea6d7f 100644
--- a/lib/pages/shopinbit/shopinbit_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_payment_view.dart
@@ -82,13 +82,26 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
if (widget.initialPaymentInfo != null) {
_applyPaymentInfo(widget.initialPaymentInfo!);
}
- // Poll even when the pre-load returned null so the view can still recover
- // a live invoice on its own.
if (widget.model.apiTicketId != 0) {
- _startPolling();
+ // If the pre-load didn't hand us usable payment links, recover them:
+ // GET, then PUT to generate one.
+ if (_addresses.every((a) => a.isEmpty)) {
+ unawaited(_recoverPaymentInfo());
+ } else {
+ _startPolling();
+ }
}
}
+ Future<void> _recoverPaymentInfo() async {
+ final info = await fetchShopInBitPaymentInfo(ref, widget.model.apiTicketId);
+ if (!mounted) return;
+ if (info != null) {
+ setState(() => _applyPaymentInfo(info));
+ }
+ _startPolling();
+ }
+
@override
void dispose() {
_pollTimer?.cancel();
@@ -230,13 +243,18 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
}
if (!mounted) return;
- widget.model.status = ShopInBitOrderStatus.paymentPending;
- widget.model.paymentMethod = method;
-
- if (Util.isDesktop) {
- Navigator.of(context, rootNavigator: true).pop();
- } else {
- Navigator.of(context).popUntil((route) => route.isFirst);
+ // Couldn't launch the in-wallet send.
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message:
+ "Payment details for $ticker aren't ready yet. "
+ "Please wait a moment or refresh the invoice.",
+ context: context,
+ ),
+ );
+ if (!_isTerminal) {
+ _startPolling();
}
}
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.