fix(shopinbit): only mark car research complete once the ticket exists
What changed, and why it matters
This commit fixes a UI timing bug in a cryptocurrency wallet's car-research payment flow. Previously, the app could mark the payment as 'complete' and try to open the order details before the backend ticket actually existed, which could briefly show the wrong screen or a fallback. Now it waits until the real ticket ID is available before marking the flow complete. There is no direct evidence this is a security vulnerability.
Treat as a routine bug fix. Review whether removing the `error` state hides legitimate failure modes (e.g., network or server errors during finalization) that previously surfaced a user-visible error dialog. If so, reintroduce error handling without regressing the timing fix.
Security signals we found
State-machine change in payment flow UI
Removal of error-state handling without replacement error path visible in diff
No changes to secrets, crypto, network trust, or access control
Evidence from the diff
The patch removes the _PaymentFlowState.error state and restructures _finalizePayment() in shopinbit_car_research_payment_view.dart. The old code set state to complete unconditionally and then either navigated to ShopInBitOrderCreated with _realTicketId or showed a fallback dialog if the ID was null. The new code only sets complete when _realTicketId is non-null; otherwise it stays in finalizing and shows the fallback. This prevents marking the flow complete before the corresponding backend ticket exists. The change is a correctness/UX fix; no cryptographic, authentication, or authorization logic is modified.
Changed components
lib/pages/shopinbit/shopinbit_car_research_payment_view.dartShopInBit car research payment flow UIInspect captured patch +14 / −43
diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
index 0e77518..03c47cc 100644
--- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
@@ -28,7 +28,7 @@ import 'shopinbit_order_created.dart';
import 'shopinbit_payment_shared.dart';
import 'shopinbit_tickets_view.dart';
-enum _PaymentFlowState { idle, polling, finalizing, complete, error }
+enum _PaymentFlowState { idle, polling, finalizing, complete }
class ShopInBitCarResearchPaymentView extends ConsumerStatefulWidget {
const ShopInBitCarResearchPaymentView({
@@ -382,52 +382,23 @@ class _ShopInBitCarResearchPaymentViewState
Future<void> _finalizePayment() async {
if (_flowState == _PaymentFlowState.finalizing ||
- _flowState == _PaymentFlowState.complete ||
- _flowState == _PaymentFlowState.error) {
+ _flowState == _PaymentFlowState.complete) {
return;
}
- setState(() => _flowState = _PaymentFlowState.finalizing);
-
- try {
- // The finalized status carries the real car ticket id (the customer
- // chat), so open that. The BTCPay webhook creates the ticket regardless.
- // The caller (_pollStatus) cancels the poll timer before calling this.
- final int? realId = _realTicketId;
-
- setState(() => _flowState = _PaymentFlowState.complete);
-
- if (realId != null) {
- unawaited(
- Navigator.of(
- context,
- ).pushNamed(ShopInBitOrderCreated.routeName, arguments: realId),
- );
- } else {
- // The real ticket hasn't surfaced yet; offer a shortcut to the
- // requests list, which will pick it up on its next refresh.
- await _showFinalizingFallback();
- }
- } catch (e, s) {
- Logging.instance.e(
- "Failed to process car research payment",
- error: e,
- stackTrace: s,
- );
- if (mounted) {
- setState(() => _flowState = _PaymentFlowState.error);
- await showDialog<void>(
- context: context,
- useRootNavigator: Util.isDesktop,
- builder: (context) => StackOkDialog(
- title: "Failed to process car research payment",
- maxWidth: Util.isDesktop ? 500 : null,
- message: e.toString(),
- desktopPopRootNavigator: Util.isDesktop,
- ),
- );
- }
+ final int? realId = _realTicketId;
+ if (realId == null) {
+ setState(() => _flowState = _PaymentFlowState.finalizing);
+ await _showFinalizingFallback();
+ return;
}
+
+ setState(() => _flowState = _PaymentFlowState.complete);
+ unawaited(
+ Navigator.of(
+ context,
+ ).pushNamed(ShopInBitOrderCreated.routeName, arguments: realId),
+ );
}
void _copyAddress(BuildContext context) {
Why this scored 16/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.