fix(shopinbit): fix resume flow and PAY NOW dialog behavior
What changed, and why it matters
This commit fixes the checkout flow for a third-party shopping service inside the Stack Wallet app. It mainly corrects navigation behavior after a user pays, prevents duplicate database records, and changes the service type label sent to the backend from 'car_research' to 'car'. There is no clear security vulnerability being patched; it reads like a bug-fix for a broken purchase/resume experience.
Treat as a routine functional bug-fix. Reviewers may optionally verify that the new resume path cannot be triggered with a stale or attacker-controlled feeTicketNumber, and that deleting the old sentinel ticket does not remove data needed elsewhere. No urgent security action is indicated by the diff alone.
Security signals we found
No explicit security claim in commit title or message
No CVE, advisory, or researcher attribution present in commit
Changes reduce state duplication and prevent spurious DB records
Adds error handling and retry UI for failed backend request submission
No cryptographic, authentication, or input-validation changes observed
Evidence from the diff
The patch refactors the ShopinBit car-research payment flow. Key changes: (1) keeps the payment polling timer running while the wallet send dialog is open and returns to the payment view on success instead of popping it; (2) persists the fee ticket number on the existing order model rather than creating a separate ‘fee receipt’ ticket, and deletes the old sentinel ticket when a real request ticket is created; (3) adds a resume path that skips fee logging if a fee ticket already exists; (4) changes the createRequest serviceType parameter from ‘car_research’ to ‘car’; (5) adds an error state guard and retry dialog. The diff is large but mostly formatting and UI wiring.
Changed components
lib/pages/shopinbit/shopinbit_car_research_payment_view.dartlib/pages/shopinbit/shopinbit_send_from_view.dartlib/pages/shopinbit/shopinbit_step_4.dartInspect captured patch +271 / −184
diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
index 2365814..0073ee8 100644
--- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
@@ -28,6 +28,8 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/desktop/secondary_button.dart';
+import '../../widgets/stack_dialog.dart';
import '../../widgets/qr.dart';
import '../../widgets/rounded_white_container.dart';
import 'shopinbit_order_created.dart';
@@ -92,10 +94,11 @@ class _ShopInBitCarResearchPaymentViewState
return _terminalStates.contains(s);
}
- bool get _payNowEnabled => !_isTerminal && _flowState == _PaymentFlowState.idle;
+ bool get _payNowEnabled =>
+ !_isTerminal && _flowState == _PaymentFlowState.idle;
void _confirmPayment() {
- _pollTimer?.cancel();
+ // Keep polling while the user is in the send flow.
final method = _methods[_selectedMethod];
final ticker = method.toUpperCase();
@@ -193,7 +196,7 @@ class _ShopInBitCarResearchPaymentViewState
EthContract? tokenContract,
}) {
if (Util.isDesktop) {
- Navigator.of(context, rootNavigator: true).pop();
+ // Show send-from on top of the payment dialog, not instead of it.
unawaited(
showDialog<void>(
context: context,
@@ -217,6 +220,8 @@ class _ShopInBitCarResearchPaymentViewState
address: address,
model: widget.model,
tokenContract: tokenContract,
+ // After wallet send, pop back to this view to continue polling.
+ routeOnSuccessName: ShopInBitCarResearchPaymentView.routeName,
),
settings: const RouteSettings(name: ShopInBitSendFromView.routeName),
),
@@ -388,7 +393,125 @@ class _ShopInBitCarResearchPaymentViewState
// Guard: only one entry allowed
if (_flowState == _PaymentFlowState.loggingPayment ||
_flowState == _PaymentFlowState.creatingRequest ||
- _flowState == _PaymentFlowState.complete) return;
+ _flowState == _PaymentFlowState.complete ||
+ _flowState == _PaymentFlowState.error)
+ return;
+
+ // Skip logCarResearchPayment if the fee was already logged.
+ final existingFeeTicket = widget.model.feeTicketNumber;
+ if (existingFeeTicket != null) {
+ if (!widget.model.needsCreateRequest) {
+ // Both steps already done: navigate to success directly.
+ if (!mounted) return;
+ setState(() => _flowState = _PaymentFlowState.complete);
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ unawaited(
+ showDialog<void>(
+ context: context,
+ builder: (_) => ShopInBitOrderCreated(model: widget.model),
+ ),
+ );
+ } else {
+ unawaited(
+ Navigator.of(context).pushNamed(
+ ShopInBitOrderCreated.routeName,
+ arguments: widget.model,
+ ),
+ );
+ }
+ return;
+ }
+ // Fee logged; skip to createRequest.
+ setState(() => _flowState = _PaymentFlowState.creatingRequest);
+ _pollTimer?.cancel();
+ try {
+ final customerKey = await ShopInBitService.instance.ensureCustomerKey();
+ final comment =
+ "${widget.model.requestDescription}\n\n"
+ "The Client paid the car research fee (#$existingFeeTicket)";
+ final reqResp = await ShopInBitService.instance.client.createRequest(
+ customerPseudonym: widget.model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car",
+ comment: comment,
+ deliveryCountry: widget.model.deliveryCountry,
+ );
+ if (reqResp.hasError || reqResp.value == null) {
+ if (mounted) {
+ setState(() => _flowState = _PaymentFlowState.error);
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (ctx) => StackDialog(
+ title: "Request Failed",
+ message:
+ "Payment was confirmed but we couldn't submit your car "
+ "research request. You can retry from My Requests.\n\n"
+ "Error: ${reqResp.exception?.message ?? 'Unknown error'}",
+ leftButton: SecondaryButton(
+ label: "Retry Now",
+ onPressed: () {
+ Navigator.of(ctx).pop();
+ _retryCreateRequest(existingFeeTicket, customerKey);
+ },
+ ),
+ rightButton: PrimaryButton(
+ label: "My Requests",
+ onPressed: () {
+ Navigator.of(ctx).pop();
+ _popToTickets();
+ },
+ ),
+ ),
+ );
+ }
+ return;
+ }
+ final requestRef = reqResp.value!;
+ final prevTicketId = widget.model.ticketId;
+ widget.model.apiTicketId = requestRef.id;
+ widget.model.ticketId = requestRef.number;
+ widget.model.status = ShopInBitOrderStatus.pending;
+ widget.model.isPendingPayment = false;
+ widget.model.needsCreateRequest = false;
+ await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
+ // Remove the sentinel record.
+ if (prevTicketId != null && prevTicketId != widget.model.ticketId) {
+ await MainDB.instance.deleteShopInBitTicket(prevTicketId);
+ }
+ if (!mounted) return;
+ setState(() => _flowState = _PaymentFlowState.complete);
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ unawaited(
+ showDialog<void>(
+ context: context,
+ builder: (_) => ShopInBitOrderCreated(model: widget.model),
+ ),
+ );
+ } else {
+ unawaited(
+ Navigator.of(context).pushNamed(
+ ShopInBitOrderCreated.routeName,
+ arguments: widget.model,
+ ),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() => _flowState = _PaymentFlowState.error);
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: e.toString(),
+ context: context,
+ ),
+ );
+ }
+ }
+ return;
+ }
setState(() => _flowState = _PaymentFlowState.loggingPayment);
_pollTimer?.cancel();
@@ -412,31 +535,23 @@ class _ShopInBitCarResearchPaymentViewState
final feeResult = logResp.value!;
- // Step 2: Persist fee receipt ticket
- final feeModel = ShopInBitOrderModel()
- ..ticketId = feeResult.ticketNumber
- ..apiTicketId = feeResult.ticketId
- ..category = ShopInBitCategory.car
- ..status = ShopInBitOrderStatus.pending
- ..displayName = widget.model.displayName
- ..requestDescription = "Car research fee receipt"
- ..deliveryCountry = widget.model.deliveryCountry
- ..needsCreateRequest = true
- ..carResearchInvoiceId = widget.invoice.btcpayInvoice
- ..feeTicketNumber = feeResult.ticketNumber;
- await MainDB.instance.putShopInBitTicket(feeModel.toIsarTicket());
+ // Persist feeTicketNumber on the existing model (a new DB row creates a spurious list entry).
+ widget.model.feeTicketNumber = feeResult.ticketNumber;
+ widget.model.needsCreateRequest = true;
+ await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
if (!mounted) return;
setState(() => _flowState = _PaymentFlowState.creatingRequest);
final customerKey = await ShopInBitService.instance.ensureCustomerKey();
- final comment = "${widget.model.requestDescription}\n\n"
+ final comment =
+ "${widget.model.requestDescription}\n\n"
"The Client paid the car research fee (#${feeResult.ticketNumber})";
final reqResp = await ShopInBitService.instance.client.createRequest(
customerPseudonym: widget.model.displayName,
externalCustomerKey: customerKey,
- serviceType: "car_research",
+ serviceType: "car",
comment: comment,
deliveryCountry: widget.model.deliveryCountry,
);
@@ -448,47 +563,43 @@ class _ShopInBitCarResearchPaymentViewState
await showDialog<void>(
context: context,
barrierDismissible: false,
- builder: (ctx) => AlertDialog(
- title: const Text("Request Failed"),
- content: Text(
- "Payment was confirmed but we couldn't submit your car "
- "research request. You can retry from My Requests.\n\n"
- "Error: ${reqResp.exception?.message ?? 'Unknown error'}",
+ builder: (ctx) => StackDialog(
+ title: "Request Failed",
+ message:
+ "Payment was confirmed but we couldn't submit your car "
+ "research request. You can retry from My Requests.\n\n"
+ "Error: ${reqResp.exception?.message ?? 'Unknown error'}",
+ leftButton: SecondaryButton(
+ label: "Retry Now",
+ onPressed: () {
+ Navigator.of(ctx).pop();
+ _retryCreateRequest(feeResult.ticketNumber, customerKey);
+ },
+ ),
+ rightButton: PrimaryButton(
+ label: "My Requests",
+ onPressed: () {
+ Navigator.of(ctx).pop();
+ _popToTickets();
+ },
),
- actions: [
- TextButton(
- onPressed: () {
- Navigator.of(ctx).pop();
- _retryCreateRequest(feeResult.ticketNumber, customerKey);
- },
- child: const Text("Retry Now"),
- ),
- TextButton(
- onPressed: () {
- Navigator.of(ctx).pop();
- _popToTickets();
- },
- child: const Text("Go to My Requests"),
- ),
- ],
),
);
}
return;
}
- // Step 4: Persist request ticket and clear pending payment state
final requestRef = reqResp.value!;
+ final prevTicketId = widget.model.ticketId;
widget.model.apiTicketId = requestRef.id;
widget.model.ticketId = requestRef.number;
widget.model.status = ShopInBitOrderStatus.pending;
- // Flow complete: clear the resume flag before saving.
widget.model.isPendingPayment = false;
+ widget.model.needsCreateRequest = false;
await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
-
- // Step 5: Update fee receipt — mark createRequest as done
- feeModel.needsCreateRequest = false;
- await MainDB.instance.putShopInBitTicket(feeModel.toIsarTicket());
+ if (prevTicketId != null && prevTicketId != widget.model.ticketId) {
+ await MainDB.instance.deleteShopInBitTicket(prevTicketId);
+ }
if (!mounted) return;
setState(() => _flowState = _PaymentFlowState.complete);
@@ -503,10 +614,9 @@ class _ShopInBitCarResearchPaymentViewState
);
} else {
unawaited(
- Navigator.of(context).pushNamed(
- ShopInBitOrderCreated.routeName,
- arguments: widget.model,
- ),
+ Navigator.of(
+ context,
+ ).pushNamed(ShopInBitOrderCreated.routeName, arguments: widget.model),
);
}
} catch (e) {
@@ -531,13 +641,14 @@ class _ShopInBitCarResearchPaymentViewState
setState(() => _flowState = _PaymentFlowState.creatingRequest);
try {
- final comment = "${widget.model.requestDescription}\n\n"
+ final comment =
+ "${widget.model.requestDescription}\n\n"
"The Client paid the car research fee (#$feeTicketNumber)";
final reqResp = await ShopInBitService.instance.client.createRequest(
customerPseudonym: widget.model.displayName,
externalCustomerKey: customerKey,
- serviceType: "car_research",
+ serviceType: "car",
comment: comment,
deliveryCountry: widget.model.deliveryCountry,
);
@@ -565,9 +676,9 @@ class _ShopInBitCarResearchPaymentViewState
await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
// Update fee receipt ticket
- final feeTickets = MainDB.instance
- .getShopInBitTickets()
- .where((t) => t.ticketId == feeTicketNumber);
+ final feeTickets = MainDB.instance.getShopInBitTickets().where(
+ (t) => t.ticketId == feeTicketNumber,
+ );
if (feeTickets.isNotEmpty) {
final feeTicket = feeTickets.first;
feeTicket.needsCreateRequest = false;
@@ -587,10 +698,9 @@ class _ShopInBitCarResearchPaymentViewState
);
} else {
unawaited(
- Navigator.of(context).pushNamed(
- ShopInBitOrderCreated.routeName,
- arguments: widget.model,
- ),
+ Navigator.of(
+ context,
+ ).pushNamed(ShopInBitOrderCreated.routeName, arguments: widget.model),
);
}
} catch (e) {
@@ -673,9 +783,9 @@ class _ShopInBitCarResearchPaymentViewState
border: Border(
bottom: BorderSide(
color: isSelected
- ? Theme.of(context)
- .extension<StackColors>()!
- .accentColorBlue
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorBlue
: Colors.transparent,
width: 2,
),
@@ -696,9 +806,7 @@ class _ShopInBitCarResearchPaymentViewState
.extension<StackColors>()!
.accentColorBlue
: null,
- fontWeight: isSelected
- ? FontWeight.w600
- : null,
+ fontWeight: isSelected ? FontWeight.w600 : null,
),
),
),
@@ -755,9 +863,9 @@ class _ShopInBitCarResearchPaymentViewState
: STextStyles.itemSubtitle12(context))
.copyWith(
color: _isTerminal
- ? Theme.of(context)
- .extension<StackColors>()!
- .accentColorGreen
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorGreen
: null,
fontWeight: _isTerminal ? FontWeight.w600 : null,
),
@@ -834,9 +942,9 @@ class _ShopInBitCarResearchPaymentViewState
label: _flowState == _PaymentFlowState.polling
? "Checking..."
: (_flowState == _PaymentFlowState.loggingPayment ||
- _flowState == _PaymentFlowState.creatingRequest)
- ? "Processing..."
- : (hasWallets ? "PAY NOW" : "CHECK FOR PAYMENT"),
+ _flowState == _PaymentFlowState.creatingRequest)
+ ? "Processing..."
+ : (hasWallets ? "PAY NOW" : "CHECK FOR PAYMENT"),
enabled: _payNowEnabled,
onPressed: _payNowEnabled
? (hasWallets
@@ -844,7 +952,7 @@ class _ShopInBitCarResearchPaymentViewState
: () => unawaited(_checkForPayment()))
: null,
),
-],
+ ],
);
if (isDesktop) {
@@ -893,9 +1001,7 @@ class _ShopInBitCarResearchPaymentViewState
context,
).extension<StackColors>()!.background,
appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: _popToTickets,
- ),
+ leading: AppBarBackButton(onPressed: _popToTickets),
title: Text("ShopinBit", style: STextStyles.navBarTitle(context)),
),
body: SafeArea(
diff --git a/lib/pages/shopinbit/shopinbit_send_from_view.dart b/lib/pages/shopinbit/shopinbit_send_from_view.dart
index 39b731a..0060cf5 100644
--- a/lib/pages/shopinbit/shopinbit_send_from_view.dart
+++ b/lib/pages/shopinbit/shopinbit_send_from_view.dart
@@ -50,6 +50,7 @@ class ShopInBitSendFromView extends ConsumerStatefulWidget {
required this.address,
this.shouldPopRoot = false,
this.tokenContract,
+ this.routeOnSuccessName,
});
static const String routeName = "/shopInBitSendFrom";
@@ -60,6 +61,8 @@ class ShopInBitSendFromView extends ConsumerStatefulWidget {
final ShopInBitOrderModel model;
final bool shouldPopRoot;
final EthContract? tokenContract;
+ // If set, overrides the default success route (HomeView/DesktopHomeView).
+ final String? routeOnSuccessName;
@override
ConsumerState<ShopInBitSendFromView> createState() =>
@@ -195,6 +198,7 @@ class _ShopInBitSendFromViewState extends ConsumerState<ShopInBitSendFromView> {
address: address,
model: model,
tokenContract: tokenContract,
+ routeOnSuccessName: widget.routeOnSuccessName,
),
);
},
@@ -215,6 +219,7 @@ class ShopInBitSendFromCard extends ConsumerStatefulWidget {
required this.address,
required this.model,
this.tokenContract,
+ this.routeOnSuccessName,
});
final String walletId;
@@ -222,6 +227,7 @@ class ShopInBitSendFromCard extends ConsumerStatefulWidget {
final String address;
final ShopInBitOrderModel model;
final EthContract? tokenContract;
+ final String? routeOnSuccessName;
@override
ConsumerState<ShopInBitSendFromCard> createState() =>
@@ -369,9 +375,11 @@ class _ShopInBitSendFromCardState extends ConsumerState<ShopInBitSendFromCard> {
builder: (_) => ShopInBitConfirmSendView(
txData: txData,
walletId: walletId,
- routeOnSuccessName: Util.isDesktop
- ? DesktopHomeView.routeName
- : HomeView.routeName,
+ routeOnSuccessName:
+ widget.routeOnSuccessName ??
+ (Util.isDesktop
+ ? DesktopHomeView.routeName
+ : HomeView.routeName),
model: model,
tokenContract: tokenContract,
),
diff --git a/lib/pages/shopinbit/shopinbit_step_4.dart b/lib/pages/shopinbit/shopinbit_step_4.dart
index 778cc8c..ea37578 100644
--- a/lib/pages/shopinbit/shopinbit_step_4.dart
+++ b/lib/pages/shopinbit/shopinbit_step_4.dart
@@ -240,15 +240,14 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
_selectedCountryIso != null;
}
if (cat == ShopInBitCategory.travel) {
- final travelBudgetVal =
- int.tryParse(_travelBudgetController.text.trim());
+ final travelBudgetVal = int.tryParse(_travelBudgetController.text.trim());
final hasValidDates = _selectedDateMode == "Flexible dates"
? (_selectedYear != null &&
- _selectedMonthSeason != null &&
- _tripLengthController.text.trim().isNotEmpty)
+ _selectedMonthSeason != null &&
+ _tripLengthController.text.trim().isNotEmpty)
: (_selectedDateMode == "Exact dates" &&
- _departureDateController.text.trim().isNotEmpty &&
- _returnDateController.text.trim().isNotEmpty);
+ _departureDateController.text.trim().isNotEmpty &&
+ _returnDateController.text.trim().isNotEmpty);
return !_submitting &&
_privacyAccepted &&
_selectedArrangement != null &&
@@ -465,8 +464,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
// encodes as Latin-1, corrupting the JSON body on mobile.
final countryIso = _selectedCountryIso!;
if (widget.model.category == ShopInBitCategory.concierge) {
- final budgetText =
- _noLimit ? "No limit" : "${_budgetController.text.trim()} EUR";
+ final budgetText = _noLimit
+ ? "No limit"
+ : "${_budgetController.text.trim()} EUR";
widget.model.requestDescription =
"What to purchase: ${_whatToPurchaseController.text.trim()}\n"
"Condition: $_selectedCondition\n"
@@ -481,7 +481,6 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
"Budget: ${_carBudgetController.text.trim()} EUR\n"
"Delivery country: $countryIso";
} else if (widget.model.category == ShopInBitCategory.travel) {
-
final parts = <String>[
"Arrangement: $_selectedArrangement",
"Departure: ${_departureCityController.text.trim()}, "
@@ -491,22 +490,23 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
if (_needsRecommendations) {
parts.add("Destinations: Recommendations requested");
} else {
- parts.add(
- "Destinations: ${_destinationsController.text.trim()}");
+ parts.add("Destinations: ${_destinationsController.text.trim()}");
}
if (_selectedDateMode == "Exact dates") {
final flex =
_selectedFlexibility != null && _selectedFlexibility != "Exact"
- ? " ($_selectedFlexibility)"
- : "";
+ ? " ($_selectedFlexibility)"
+ : "";
parts.add(
- "Dates: ${_departureDateController.text.trim()} - "
- "${_returnDateController.text.trim()}$flex");
+ "Dates: ${_departureDateController.text.trim()} - "
+ "${_returnDateController.text.trim()}$flex",
+ );
} else if (_selectedDateMode == "Flexible dates") {
parts.add(
- "Dates: $_selectedMonthSeason $_selectedYear, "
- "${_tripLengthController.text.trim()} nights");
+ "Dates: $_selectedMonthSeason $_selectedYear, "
+ "${_tripLengthController.text.trim()} nights",
+ );
}
final travelers = <String>[];
@@ -590,10 +590,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
);
} else {
unawaited(
- Navigator.of(context).pushNamed(
- ShopInBitCarFeeView.routeName,
- arguments: widget.model,
- ),
+ Navigator.of(
+ context,
+ ).pushNamed(ShopInBitCarFeeView.routeName, arguments: widget.model),
);
}
return;
@@ -680,9 +679,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
// Shared widgets.
Widget _buildCountryPicker(bool isDesktop) {
return ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
+ borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: _selectedCountryIso,
@@ -693,9 +690,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
child: Text(
c['label'] as String,
style: isDesktop
- ? STextStyles.desktopTextExtraSmall(
- context,
- ).copyWith(
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textFieldActiveText,
@@ -721,9 +716,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
_loadingCountries ? "Loading countries..." : "Delivery country",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldDefaultSearchIconLeft,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultSearchIconLeft,
)
: STextStyles.fieldLabel(context),
),
@@ -785,9 +780,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
.where((c) => c['iso'] == item.value)
.map((c) => c['label'] as String)
.firstOrNull;
- return label?.toLowerCase().contains(
- searchValue.toLowerCase(),
- ) ??
+ return label?.toLowerCase().contains(searchValue.toLowerCase()) ??
false;
},
),
@@ -881,7 +874,8 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
// Per-category form builders.
Widget _buildConciergeContent(bool isDesktop) {
- final whatToPurchaseError = _whatToPurchaseTouched &&
+ final whatToPurchaseError =
+ _whatToPurchaseTouched &&
_whatToPurchaseController.text.trim().length < 10
? "Minimum 10 characters"
: null;
@@ -969,9 +963,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
).copyWith(
color: Theme.of(
context,
- )
- .extension<StackColors>()!
- .textFieldActiveText,
+ ).extension<StackColors>()!.textFieldActiveText,
)
: STextStyles.w500_14(context),
),
@@ -1124,24 +1116,24 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
}
Widget _buildCarContent(bool isDesktop) {
- final brandError =
- _brandTouched && _brandController.text.trim().length < 3
- ? "Minimum 3 characters"
- : null;
+ final brandError = _brandTouched && _brandController.text.trim().length < 3
+ ? "Minimum 3 characters"
+ : null;
- final modelError =
- _modelTouched && _modelController.text.trim().length < 3
- ? "Minimum 3 characters"
- : null;
+ final modelError = _modelTouched && _modelController.text.trim().length < 3
+ ? "Minimum 3 characters"
+ : null;
- final carDescriptionError = _carDescriptionTouched &&
+ final carDescriptionError =
+ _carDescriptionTouched &&
_carDescriptionController.text.trim().length < 3
? "Minimum 3 characters"
: null;
final carBudgetText = _carBudgetController.text.trim();
final carBudgetVal = int.tryParse(carBudgetText);
- final carBudgetError = _carBudgetTouched &&
+ final carBudgetError =
+ _carBudgetTouched &&
(carBudgetText.isEmpty ||
carBudgetVal == null ||
carBudgetVal < 20000)
@@ -1261,9 +1253,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
).copyWith(
color: Theme.of(
context,
- )
- .extension<StackColors>()!
- .textFieldActiveText,
+ ).extension<StackColors>()!.textFieldActiveText,
)
: STextStyles.w500_14(context),
),
@@ -1401,7 +1391,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
// Research fee info box
RoundedWhiteContainer(
child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
@@ -1421,12 +1411,12 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
TextSpan(
text: "Research fee: ",
style: isDesktop
- ? STextStyles.desktopTextSmall(context).copyWith(
- fontWeight: FontWeight.bold,
- )
- : STextStyles.w500_14(context).copyWith(
- fontWeight: FontWeight.bold,
- ),
+ ? STextStyles.desktopTextSmall(
+ context,
+ ).copyWith(fontWeight: FontWeight.bold)
+ : STextStyles.w500_14(
+ context,
+ ).copyWith(fontWeight: FontWeight.bold),
),
const TextSpan(
text:
@@ -1579,9 +1569,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
required bool isDesktop,
}) {
return ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
+ borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: value,
@@ -1592,14 +1580,10 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
child: Text(
c,
style: isDesktop
- ? STextStyles.desktopTextExtraSmall(
- context,
- ).copyWith(
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(
context,
- )
- .extension<StackColors>()!
- .textFieldActiveText,
+ ).extension<StackColors>()!.textFieldActiveText,
)
: STextStyles.w500_14(context),
),
@@ -1611,9 +1595,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
hint,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldDefaultSearchIconLeft,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultSearchIconLeft,
)
: STextStyles.fieldLabel(context),
),
@@ -1679,9 +1663,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
),
const Spacer(),
InkWell(
- onTap: value > min
- ? () => onChanged(value - 1)
- : null,
+ onTap: value > min ? () => onChanged(value - 1) : null,
child: Container(
width: 32,
height: 32,
@@ -1717,9 +1699,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
),
const SizedBox(width: 16),
InkWell(
- onTap: value < max
- ? () => onChanged(value + 1)
- : null,
+ onTap: value < max ? () => onChanged(value + 1) : null,
child: Container(
width: 32,
height: 32,
@@ -1746,40 +1726,43 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
}
Widget _buildTravelContent(bool isDesktop) {
- final departureCountryError = _departureCountryTouched &&
+ final departureCountryError =
+ _departureCountryTouched &&
_departureCountryController.text.trim().isEmpty
? "Required"
: null;
- final departureCityError = _departureCityTouched &&
- _departureCityController.text.trim().isEmpty
+ final departureCityError =
+ _departureCityTouched && _departureCityController.text.trim().isEmpty
? "Required"
: null;
- final destinationsError = _destinationsTouched &&
+ final destinationsError =
+ _destinationsTouched &&
_destinationsController.text.trim().isEmpty &&
!_needsRecommendations
? "Required (or check 'I need recommendations')"
: null;
- final departureDateError = _departureDateTouched &&
- _departureDateController.text.trim().isEmpty
+ final departureDateError =
+ _departureDateTouched && _departureDateController.text.trim().isEmpty
? "Required"
: null;
- final returnDateError = _returnDateTouched &&
- _returnDateController.text.trim().isEmpty
+ final returnDateError =
+ _returnDateTouched && _returnDateController.text.trim().isEmpty
? "Required"
: null;
- final tripLengthError = _tripLengthTouched &&
- _tripLengthController.text.trim().isEmpty
+ final tripLengthError =
+ _tripLengthTouched && _tripLengthController.text.trim().isEmpty
? "Required"
: null;
final travelBudgetText = _travelBudgetController.text.trim();
final travelBudgetVal = int.tryParse(travelBudgetText);
- final travelBudgetError = _travelBudgetTouched &&
+ final travelBudgetError =
+ _travelBudgetTouched &&
(travelBudgetText.isEmpty ||
travelBudgetVal == null ||
travelBudgetVal < 1000)
@@ -2058,8 +2041,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
"+ 1 week",
],
hint: "Flexibility",
- onChanged: (val) =>
- setState(() => _selectedFlexibility = val),
+ onChanged: (val) => setState(() => _selectedFlexibility = val),
isDesktop: isDesktop,
),
],
@@ -2067,13 +2049,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
if (_selectedDateMode == "Flexible dates") ...[
_buildTravelDropdown(
value: _selectedYear,
- items: [
- "${DateTime.now().year}",
- "${DateTime.now().year + 1}",
- ],
+ items: ["${DateTime.now().year}", "${DateTime.now().year + 1}"],
hint: "Year",
- onChanged: (val) =>
- setState(() => _selectedYear = val),
+ onChanged: (val) => setState(() => _selectedYear = val),
isDesktop: isDesktop,
),
SizedBox(height: isDesktop ? 16 : 12),
@@ -2098,8 +2076,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
"Winter (Dec-Feb)",
],
hint: "Month or season",
- onChanged: (val) =>
- setState(() => _selectedMonthSeason = val),
+ onChanged: (val) => setState(() => _selectedMonthSeason = val),
isDesktop: isDesktop,
),
SizedBox(height: isDesktop ? 16 : 12),
@@ -2268,10 +2245,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
iconSize: 23,
onPressed: _popBack,
),
- Text(
- "ShopinBit",
- style: STextStyles.desktopH3(context),
- ),
+ Text("ShopinBit", style: STextStyles.desktopH3(context)),
],
),
const DesktopDialogCloseButton(),
@@ -2300,12 +2274,11 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
}
},
child: Scaffold(
- backgroundColor:
- Theme.of(context).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: _popBack,
- ),
+ leading: AppBarBackButton(onPressed: _popBack),
title: Text("ShopinBit", style: STextStyles.navBarTitle(context)),
),
body: SafeArea(
Why this scored 23/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.