fix(shopinbit): mobile back-button handling and car research payment flow
What changed, and why it matters
This commit fixes the mobile back-button behavior and the payment flow for a car-research feature inside the Stack Wallet app. It changes how the app figures out the payment amount (reading it from a BIP21 payment URI instead of an API field that no longer exists) and adds a 'PAY NOW' button that can launch the wallet's own send screen. There is no clear security bug being patched; it looks like a routine functional fix for a partner integration.
Treat as a normal feature/bug-fix review. Verify that the new BIP21 URI parsing correctly handles edge cases (missing amount, malformed URIs, duplicate query keys) and that the hard-coded USDT contract address matches the intended network. Confirm that PopScope back-button interception does not create navigation loops or prevent users from exiting the app. No security patch urgency is evident from the supplied materials.
Security signals we found
No security-relevant keywords in commit title or message
No CVE, advisory, or researcher attribution in commit or references
Functional change: back-button navigation interception via PopScope
Functional change: amount/fee sourcing moved from API response to BIP21 URI parsing
Functional change: new in-app payment path that constructs wallet send transactions
Hard-coded USDT ERC-20 contract address introduced (0xdac17f958d2ee523a2206206994597c13d831ec7)
Evidence from the diff
The patch updates four ShopInBit Dart files. It wraps several pages in PopScope(canPop: false) and routes back-button presses to specific earlier pages. It replaces the previous fee/amount lookup from the car-research invoice status response with parsing of the BIP21 amount query parameter from invoice.paymentLinks, falling back to a hard-coded ‘50.00 EUR’. It also rewrites the payment view to support an in-app ‘PAY NOW’ path: it parses the selected payment URI, resolves a CryptoCurrency or USDT ERC-20 contract, and navigates to ShopInBitSendFromView with the address and amount. A ‘CHECK FOR PAYMENT’ path remains for unsupported wallets. No explicit vulnerability disclosure, CVE, or security advisory is present in the supplied materials.
Changed components
lib/pages/shopinbit/shopinbit_car_fee_view.dartlib/pages/shopinbit/shopinbit_car_research_payment_view.dartlib/pages/shopinbit/shopinbit_step_2.dartlib/pages/shopinbit/shopinbit_step_4.dartInspect captured patch +475 / −109
diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
index 82aa18e..13a2378 100644
--- a/lib/pages/shopinbit/shopinbit_car_fee_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
@@ -12,8 +12,10 @@ import '../../services/shopinbit/src/models/car_research.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
+import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
+import '../more_view/services_view.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
@@ -22,6 +24,7 @@ import '../../widgets/desktop/primary_button.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_text_field.dart';
import 'shopinbit_car_research_payment_view.dart';
+import 'shopinbit_step_2.dart';
class ShopInBitCarFeeView extends StatefulWidget {
const ShopInBitCarFeeView({super.key, required this.model});
@@ -98,6 +101,22 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
super.dispose();
}
+ void _popToStep2() {
+ Navigator.of(context).popUntil((route) {
+ final name = route.settings.name;
+ if (name == ShopInBitStep2.routeName) {
+ return true;
+ }
+ if (name == ServicesView.routeName) {
+ return true;
+ }
+ if (route.isFirst) {
+ return true;
+ }
+ return false;
+ });
+ }
+
Future<void> _fetchCountries() async {
setState(() => _loadingBillingCountries = true);
try {
@@ -204,26 +223,61 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
}
}
+ String? _parseBip21Amount(String uri) {
+ try {
+ // Parse amount from payment URI query params.
+ final qIdx = uri.indexOf('?');
+ if (qIdx < 0) return null;
+ final query = uri.substring(qIdx + 1);
+ final params = Uri.splitQueryString(query);
+ return params['amount'] ?? params['tx_amount'];
+ } catch (_) {
+ return null;
+ }
+ }
+
Future<void> _loadFee(CarResearchInvoice invoice) async {
+ // Keep status call for visibility into any future API changes surfacing
+ // a fee field. Today the endpoint returns only {status, additional}, so
+ // we source the displayed amount from the BIP21 payment URIs instead.
try {
final resp = await ShopInBitService.instance.client
.getCarResearchInvoiceStatus(invoice.btcpayInvoice);
if (resp.hasError || resp.value == null) {
- if (mounted) setState(() => _displayedFee = "—");
- return;
+ Logging.instance.i(
+ "CarResearch status response (car_fee_view): error "
+ "${resp.exception?.message}",
+ );
+ } else {
+ Logging.instance.i(
+ "CarResearch status response (car_fee_view): ${resp.value}",
+ );
}
- final data = resp.value!;
- final parsed = (data["fee"] ??
- data["amount"] ??
- data["total"] ??
- data["customer_price"])
- ?.toString();
- if (mounted) {
- setState(() => _displayedFee = parsed ?? "—");
+ } catch (e) {
+ Logging.instance.i(
+ "CarResearch status response (car_fee_view): threw $e",
+ );
+ }
+
+ // Primary fee source: parse BIP21 `amount` query param from paymentLinks.
+ Logging.instance.i(
+ "CarResearch paymentLinks (car_fee_view): ${invoice.paymentLinks}",
+ );
+ try {
+ for (final entry in invoice.paymentLinks.entries) {
+ final parsed = _parseBip21Amount(entry.value);
+ if (parsed != null && parsed.isNotEmpty) {
+ if (mounted) {
+ setState(() => _displayedFee = "$parsed ${entry.key.toUpperCase()}");
+ }
+ return;
+ }
}
} catch (_) {
- if (mounted) setState(() => _displayedFee = "—");
+ // Leave placeholder in place.
}
+ // No parse succeeded — leave the existing "50.00 EUR" business-rule
+ // placeholder in place rather than showing "—".
}
Widget _buildField({
@@ -508,29 +562,38 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
}
return Background(
- child: Scaffold(
- backgroundColor: Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
+ child: PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (bool didPop, dynamic result) {
+ if (!didPop) {
+ _popToStep2();
+ }
+ },
+ child: Scaffold(
+ backgroundColor:
+ Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: _popToStep2,
+ ),
+ title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
),
- title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.all(16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 32,
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 32,
+ ),
+ child: IntrinsicHeight(child: content),
),
- child: IntrinsicHeight(child: content),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
),
diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
index 96c2c86..e37a07a 100644
--- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
@@ -1,17 +1,28 @@
import 'dart:async';
+import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../app_config.dart';
import '../../db/isar/main_db.dart';
+import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
+import '../../providers/providers.dart';
+import '../../route_generator.dart';
import '../../services/shopinbit/shopinbit_service.dart';
import '../../services/shopinbit/src/models/car_research.dart';
import '../../themes/stack_colors.dart';
+import '../../utilities/address_utils.dart';
+import '../../utilities/amount/amount.dart';
import '../../utilities/assets.dart';
+import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
+import '../../wallets/crypto_currency/crypto_currency.dart';
+import '../more_view/services_view.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
@@ -20,8 +31,10 @@ import '../../widgets/desktop/primary_button.dart';
import '../../widgets/qr.dart';
import '../../widgets/rounded_white_container.dart';
import 'shopinbit_order_created.dart';
+import 'shopinbit_send_from_view.dart';
+import 'shopinbit_tickets_view.dart';
-class ShopInBitCarResearchPaymentView extends StatefulWidget {
+class ShopInBitCarResearchPaymentView extends ConsumerStatefulWidget {
const ShopInBitCarResearchPaymentView({
super.key,
required this.model,
@@ -34,22 +47,30 @@ class ShopInBitCarResearchPaymentView extends StatefulWidget {
final CarResearchInvoice invoice;
@override
- State<ShopInBitCarResearchPaymentView> createState() =>
+ ConsumerState<ShopInBitCarResearchPaymentView> createState() =>
_ShopInBitCarResearchPaymentViewState();
}
class _ShopInBitCarResearchPaymentViewState
- extends State<ShopInBitCarResearchPaymentView> {
+ extends ConsumerState<ShopInBitCarResearchPaymentView> {
static const Set<String> _terminalStates = {
+ // concierge heritage
"paid",
"paid_over",
"paid_late",
"payment_processing",
+ // BTCPay / car research likely
+ "settled",
+ "confirmed",
+ "complete",
+ "completed",
+ "finalized",
};
Timer? _pollTimer;
Map<String, dynamic>? _status;
bool _logging = false;
+ bool _checking = false;
String _statusString = "ready_to_pay";
List<String> _methods = [];
List<String> _addresses = [];
@@ -58,17 +79,203 @@ class _ShopInBitCarResearchPaymentViewState
String get _currentAddress =>
_selectedMethod < _addresses.length ? _addresses[_selectedMethod] : "";
- bool get _isTerminal => _terminalStates.contains(_statusString);
+ bool get _isTerminal {
+ final s = _statusString.toLowerCase().trim();
+ return _terminalStates.contains(s);
+ }
+
+ bool get _payNowEnabled => !_isTerminal && !_logging && !_checking;
+
+ void _confirmPayment() {
+ _pollTimer?.cancel();
+ final method = _methods[_selectedMethod];
+ final ticker = method.toUpperCase();
+
+ final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
+
+ String address = "";
+ Amount? amount;
+ EthContract? tokenContract;
+
+ if (_currentAddress.isNotEmpty) {
+ final parsed = AddressUtils.parsePaymentUri(_currentAddress);
+
+ if (parsed?.address != null && parsed!.address.isNotEmpty) {
+ address = parsed.address;
+ } else {
+ final raw = _currentAddress;
+ final colonIdx = raw.indexOf(':');
+ if (colonIdx != -1) {
+ final afterScheme = raw.substring(colonIdx + 1);
+ final qIdx = afterScheme.indexOf('?');
+ address = qIdx != -1 ? afterScheme.substring(0, qIdx) : afterScheme;
+ } else {
+ address = raw;
+ }
+ }
+
+ String? amountStr = parsed?.amount;
+ if (amountStr == null || amountStr.isEmpty) {
+ final uri = Uri.tryParse(_currentAddress);
+ if (uri != null) {
+ amountStr = uri.queryParameters['amount'];
+ }
+ }
+ // Car research flow has no concierge PaymentInfo.due fallback.
+
+ final int fractionDigits;
+ if (coin != null) {
+ fractionDigits = coin.fractionDigits;
+ } else if (ticker == "USDT") {
+ fractionDigits = 6;
+ } else {
+ fractionDigits = 8;
+ }
+
+ if (amountStr != null && amountStr.isNotEmpty) {
+ try {
+ amount = Amount.fromDecimal(
+ Decimal.parse(amountStr),
+ fractionDigits: fractionDigits,
+ );
+ } catch (_) {}
+ }
+ }
+
+ if (coin != null && address.isNotEmpty) {
+ _navigateToSendFrom(coin: coin, amount: amount, address: address);
+ return;
+ }
+
+ if (ticker == "USDT" && address.isNotEmpty) {
+ const usdtAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
+ tokenContract = ref.read(mainDBProvider).getEthContractSync(usdtAddress);
+ if (tokenContract != null) {
+ final ethCoin = AppConfig.getCryptoCurrencyForTicker("ETH");
+ if (ethCoin != null) {
+ _navigateToSendFrom(
+ coin: ethCoin,
+ amount: amount,
+ address: address,
+ tokenContract: tokenContract,
+ );
+ return;
+ }
+ }
+ }
+
+ // No compatible wallet coin found — surface an info flushbar and keep
+ // the user on this screen so they can pay externally and then use the
+ // "CHECK FOR PAYMENT" button.
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message:
+ "No compatible wallet for $method. "
+ "Pay externally, then tap CHECK FOR PAYMENT.",
+ context: context,
+ ),
+ );
+ }
+
+ void _navigateToSendFrom({
+ required CryptoCurrency coin,
+ required Amount? amount,
+ required String address,
+ EthContract? tokenContract,
+ }) {
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ unawaited(
+ showDialog<void>(
+ context: context,
+ builder: (_) => ShopInBitSendFromView(
+ coin: coin,
+ amount: amount,
+ address: address,
+ model: widget.model,
+ shouldPopRoot: true,
+ tokenContract: tokenContract,
+ ),
+ ),
+ );
+ } else {
+ Navigator.of(context).push(
+ RouteGenerator.getRoute<dynamic>(
+ shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
+ builder: (_) => ShopInBitSendFromView(
+ coin: coin,
+ amount: amount,
+ address: address,
+ model: widget.model,
+ tokenContract: tokenContract,
+ ),
+ settings: const RouteSettings(name: ShopInBitSendFromView.routeName),
+ ),
+ );
+ }
+ }
+
+ Future<void> _checkForPayment() async {
+ if (_checking || _logging) return;
+ setState(() => _checking = true);
+ try {
+ await _pollStatus();
+ if (!mounted) return;
+ if (!_isTerminal && !_logging) {
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message:
+ "Payment not yet confirmed. Please wait a moment and try again.",
+ context: context,
+ ),
+ );
+ }
+ } finally {
+ if (mounted) setState(() => _checking = false);
+ }
+ }
+
+ String? _parseBip21Amount(String uri) {
+ try {
+ // Parse amount from payment URI query params.
+ final qIdx = uri.indexOf('?');
+ if (qIdx < 0) return null;
+ final query = uri.substring(qIdx + 1);
+ final params = Uri.splitQueryString(query);
+ return params['amount'] ?? params['tx_amount'];
+ } catch (_) {
+ return null;
+ }
+ }
String get _displayedFee {
- final s = _status;
- if (s == null) return "—";
- return (s["fee"] ??
- s["amount"] ??
- s["total"] ??
- s["customer_price"] ??
- "—")
- .toString();
+ // API status endpoint does not expose a fee field (confirmed: returns
+ // only {status, additional}). Parse the amount from the BIP21 payment
+ // URI for the currently-selected method, fall back to the 50.00 EUR
+ // business-rule value if no parse succeeds.
+ final links = widget.invoice.paymentLinks;
+ if (_selectedMethod < _methods.length) {
+ final methodKey = _methods[_selectedMethod];
+ // _methods holds upper-cased keys; links map may be case-sensitive.
+ String? uri = links[methodKey];
+ if (uri == null) {
+ for (final entry in links.entries) {
+ if (entry.key.toUpperCase() == methodKey) {
+ uri = entry.value;
+ break;
+ }
+ }
+ }
+ if (uri != null) {
+ final parsed = _parseBip21Amount(uri);
+ if (parsed != null && parsed.isNotEmpty) {
+ return "$parsed $methodKey";
+ }
+ }
+ }
+ return "50.00 EUR";
}
String get _statusLabel {
@@ -105,6 +312,22 @@ class _ShopInBitCarResearchPaymentViewState
super.dispose();
}
+ void _popToTickets() {
+ Navigator.of(context).popUntil((route) {
+ final name = route.settings.name;
+ if (name == ShopInBitTicketsView.routeName) {
+ return true;
+ }
+ if (name == ServicesView.routeName) {
+ return true;
+ }
+ if (route.isFirst) {
+ return true;
+ }
+ return false;
+ });
+ }
+
Future<void> _pollStatus() async {
try {
final resp = await ShopInBitService.instance.client
@@ -123,6 +346,13 @@ class _ShopInBitCarResearchPaymentViewState
return;
}
if (!mounted) return;
+ Logging.instance.i(
+ "CarResearch status response (payment_view): ${resp.value}",
+ );
+ Logging.instance.i(
+ "CarResearch paymentLinks (payment_view): "
+ "${widget.invoice.paymentLinks}",
+ );
setState(() {
_status = resp.value!;
_statusString = _status!["status"]?.toString() ?? _statusString;
@@ -219,6 +449,31 @@ class _ShopInBitCarResearchPaymentViewState
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
+ final ticker = _selectedMethod < _methods.length
+ ? _methods[_selectedMethod].toUpperCase()
+ : "";
+
+ bool hasWallets = false;
+ if (ticker == "USDT") {
+ const usdtAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
+ hasWallets = ref
+ .watch(pWallets)
+ .wallets
+ .any(
+ (w) =>
+ w.info.coin is Ethereum &&
+ w.info.tokenContractAddresses.contains(usdtAddress),
+ );
+ } else {
+ final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
+ if (coin != null) {
+ hasWallets = ref
+ .watch(pWallets)
+ .wallets
+ .any((e) => e.info.coin == coin);
+ }
+ }
+
final methodSelector = _methods.length <= 1
? Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
@@ -399,11 +654,17 @@ class _ShopInBitCarResearchPaymentViewState
),
const Spacer(),
PrimaryButton(
- label: "I've paid",
- enabled: !_logging,
- onPressed: _logging ? null : () => unawaited(_logPayment()),
+ label: _checking
+ ? "Checking..."
+ : (hasWallets ? "PAY NOW" : "CHECK FOR PAYMENT"),
+ enabled: _payNowEnabled,
+ onPressed: _payNowEnabled
+ ? (hasWallets
+ ? _confirmPayment
+ : () => unawaited(_checkForPayment()))
+ : null,
),
- ],
+],
);
if (isDesktop) {
@@ -440,31 +701,39 @@ class _ShopInBitCarResearchPaymentViewState
}
return Background(
- child: Scaffold(
- backgroundColor: Theme.of(
- context,
- ).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
+ child: PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (bool didPop, dynamic result) {
+ if (!didPop) {
+ _popToTickets();
+ }
+ },
+ child: Scaffold(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: _popToTickets,
+ ),
+ title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
),
- title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.all(16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 32,
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 32,
+ ),
+ child: IntrinsicHeight(child: content),
),
- child: IntrinsicHeight(child: content),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
),
diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart
index 31abd1d..1fc04f4 100644
--- a/lib/pages/shopinbit/shopinbit_step_2.dart
+++ b/lib/pages/shopinbit/shopinbit_step_2.dart
@@ -36,6 +36,14 @@ class _ShopInBitStep2State extends State<ShopInBitStep2> {
_selected = null;
}
+ void _popBack() {
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ } else {
+ Navigator.of(context).pop();
+ }
+ }
+
void _continue() {
widget.model.category = _selected;
if (Util.isDesktop) {
@@ -228,29 +236,38 @@ class _ShopInBitStep2State extends State<ShopInBitStep2> {
}
return Background(
- child: Scaffold(
- backgroundColor: Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
+ child: PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (bool didPop, dynamic result) {
+ if (!didPop) {
+ _popBack();
+ }
+ },
+ child: Scaffold(
+ backgroundColor:
+ Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: _popBack,
+ ),
+ title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
),
- title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.all(16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 32,
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 32,
+ ),
+ child: IntrinsicHeight(child: content),
),
- child: IntrinsicHeight(child: content),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
),
diff --git a/lib/pages/shopinbit/shopinbit_step_4.dart b/lib/pages/shopinbit/shopinbit_step_4.dart
index 05577b8..6904b90 100644
--- a/lib/pages/shopinbit/shopinbit_step_4.dart
+++ b/lib/pages/shopinbit/shopinbit_step_4.dart
@@ -169,6 +169,14 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
super.dispose();
}
+ void _popBack() {
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ } else {
+ Navigator.of(context).pop();
+ }
+ }
+
Future<void> _fetchCountries() async {
setState(() => _loadingCountries = true);
try {
@@ -583,29 +591,38 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
}
return Background(
- child: Scaffold(
- backgroundColor: Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
+ child: PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (bool didPop, dynamic result) {
+ if (!didPop) {
+ _popBack();
+ }
+ },
+ child: Scaffold(
+ backgroundColor:
+ Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: _popBack,
+ ),
+ title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
),
- title: Text("ShopInBit", style: STextStyles.navBarTitle(context)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.all(16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 32,
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 32,
+ ),
+ child: IntrinsicHeight(child: content),
),
- child: IntrinsicHeight(child: content),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
),
Why this scored 20/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.