refactor: extract shared payment flow
What changed, and why it matters
This commit is a routine code cleanup: it moves duplicated payment-handling logic from two ShopInBit screens into a single shared helper file. There is no security-relevant change visible in the diff—no new permissions, no altered validation, no changed cryptography, and no new network behavior. The shipping screen also gets a small UI tweak to allow country selection only when the order did not already arrive with a country.
No security action required. Treat as normal refactoring review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff extracts common functions for parsing BIP21/payment URIs, checking wallet availability, and launching the in-wallet send flow into a new file shopinbit_payment_shared.dart. Both shopinbit_car_research_payment_view.dart and shopinbit_payment_view.dart now call these helpers instead of duplicating the logic. The parsing algorithm, token contract address handling, and navigation behavior are preserved. shopinbit_shipping_view.dart adds a _countryLocked flag and conditionally enables the country dropdown. No security-sensitive logic was modified.
Changed components
lib/pages/shopinbit/shopinbit_car_research_payment_view.dartlib/pages/shopinbit/shopinbit_payment_shared.dartlib/pages/shopinbit/shopinbit_payment_view.dartlib/pages/shopinbit/shopinbit_shipping_view.dartInspect captured patch +323 / −354
diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
index 0d3d3a1..40c366d 100644
--- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
@@ -1,28 +1,20 @@
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 '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/global/shopin_bit_service_provider.dart';
import '../../providers/providers.dart';
-import '../../route_generator.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 '../../widgets/background.dart';
-import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
@@ -32,7 +24,7 @@ import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../more_view/services_view.dart';
import 'shopinbit_order_created.dart';
-import 'shopinbit_send_from_view.dart';
+import 'shopinbit_payment_shared.dart';
import 'shopinbit_tickets_view.dart';
enum _PaymentFlowState {
@@ -101,78 +93,24 @@ class _ShopInBitCarResearchPaymentViewState
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 (_) {}
- }
- }
+ final target = parseShopInBitPaymentTarget(
+ paymentUri: _currentAddress,
+ ticker: ticker,
+ coin: AppConfig.getCryptoCurrencyForTicker(ticker),
+ );
- if (coin != null && address.isNotEmpty) {
- _navigateToSendFrom(coin: coin, amount: amount, address: address);
- return;
- }
+ final navigated = tryNavigateToShopInBitWalletSend(
+ ref: ref,
+ context: context,
+ ticker: ticker,
+ address: target.address,
+ amount: target.amount,
+ model: widget.model,
+ // After the wallet send, pop back here so polling can continue.
+ routeOnSuccessName: ShopInBitCarResearchPaymentView.routeName,
+ );
- 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;
- }
- }
- }
+ if (navigated) 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
@@ -188,46 +126,6 @@ class _ShopInBitCarResearchPaymentViewState
);
}
- void _navigateToSendFrom({
- required CryptoCurrency coin,
- required Amount? amount,
- required String address,
- EthContract? tokenContract,
- }) {
- if (Util.isDesktop) {
- // Show send-from on top of the payment dialog, not instead of it.
- 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,
- // After wallet send, pop back to this view to continue polling.
- routeOnSuccessName: ShopInBitCarResearchPaymentView.routeName,
- ),
- settings: const RouteSettings(name: ShopInBitSendFromView.routeName),
- ),
- );
- }
- }
-
Future<void> _checkForPayment() async {
if (_flowState != _PaymentFlowState.idle) return;
setState(() => _flowState = _PaymentFlowState.polling);
@@ -731,26 +629,7 @@ class _ShopInBitCarResearchPaymentViewState
? _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 hasWallets = hasShopInBitWalletForTicker(ref.watch(pWallets), ticker);
final methodSelector = _methods.length <= 1
? Padding(
@@ -985,41 +864,9 @@ class _ShopInBitCarResearchPaymentViewState
);
}
- return Background(
- 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)),
- ),
- 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),
- ),
- ),
- );
- },
- ),
- ),
- ),
- ),
+ return ShopInBitPaymentMobileScaffold(
+ onBack: _popToTickets,
+ child: content,
);
}
}
diff --git a/lib/pages/shopinbit/shopinbit_payment_shared.dart b/lib/pages/shopinbit/shopinbit_payment_shared.dart
new file mode 100644
index 0000000..d15e22e
--- /dev/null
+++ b/lib/pages/shopinbit/shopinbit_payment_shared.dart
@@ -0,0 +1,271 @@
+import 'dart:async';
+
+import 'package:decimal/decimal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../app_config.dart';
+import '../../models/isar/models/ethereum/eth_contract.dart';
+import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../providers/providers.dart';
+import '../../route_generator.dart';
+import '../../services/wallets.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/address_utils.dart';
+import '../../utilities/amount/amount.dart';
+import '../../utilities/default_eth_tokens.dart';
+import '../../utilities/text_styles.dart';
+import '../../utilities/util.dart';
+import '../../wallets/crypto_currency/crypto_currency.dart';
+import '../../widgets/background.dart';
+import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../widgets/loading_indicator.dart';
+import 'shopinbit_send_from_view.dart';
+
+final String kShopInBitUsdtContractAddress = DefaultTokens.list
+ .firstWhere((t) => t.symbol == "USDT")
+ .address;
+
+// Address + amount pulled out of one of the API's payment_links entries.
+class ShopInBitPaymentTarget {
+ const ShopInBitPaymentTarget({required this.address, required this.amount});
+
+ final String address;
+ final Amount? amount;
+}
+
+// Parses a BIP21-style payment URI (or a bare address) into a destination
+// address and optional Amount. `amountFallback` covers the concierge case
+// where the URI itself has no amount but the API response carries one
+// (PaymentInfo.due).
+ShopInBitPaymentTarget parseShopInBitPaymentTarget({
+ required String paymentUri,
+ required String ticker,
+ CryptoCurrency? coin,
+ String? amountFallback,
+}) {
+ String address = "";
+ final parsed = AddressUtils.parsePaymentUri(paymentUri);
+
+ if (parsed?.address != null && parsed!.address.isNotEmpty) {
+ address = parsed.address;
+ } else {
+ final colonIdx = paymentUri.indexOf(':');
+ if (colonIdx != -1) {
+ final afterScheme = paymentUri.substring(colonIdx + 1);
+ final qIdx = afterScheme.indexOf('?');
+ address = qIdx != -1 ? afterScheme.substring(0, qIdx) : afterScheme;
+ } else {
+ address = paymentUri;
+ }
+ }
+
+ String? amountStr = parsed?.amount;
+ if (amountStr == null || amountStr.isEmpty) {
+ final uri = Uri.tryParse(paymentUri);
+ if (uri != null) {
+ amountStr = uri.queryParameters['amount'];
+ }
+ }
+ if (amountStr == null || amountStr.isEmpty) {
+ amountStr = amountFallback;
+ }
+
+ final int fractionDigits;
+ if (coin != null) {
+ fractionDigits = coin.fractionDigits;
+ } else if (ticker == "USDT") {
+ fractionDigits = 6;
+ } else {
+ fractionDigits = 8;
+ }
+
+ Amount? amount;
+ if (amountStr != null && amountStr.isNotEmpty) {
+ try {
+ amount = Amount.fromDecimal(
+ Decimal.parse(amountStr),
+ fractionDigits: fractionDigits,
+ );
+ } catch (_) {}
+ }
+
+ return ShopInBitPaymentTarget(address: address, amount: amount);
+}
+
+// True if any wallet in [wallets] can send the given upper-cased [ticker].
+// USDT is special-cased to look at Ethereum wallets' token contracts.
+bool hasShopInBitWalletForTicker(Wallets wallets, String ticker) {
+ if (ticker == "USDT") {
+ return wallets.wallets.any(
+ (w) =>
+ w.info.coin is Ethereum &&
+ w.info.tokenContractAddresses.contains(kShopInBitUsdtContractAddress),
+ );
+ }
+ final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
+ if (coin == null) return false;
+ return wallets.wallets.any((e) => e.info.coin == coin);
+}
+
+void _pushShopInBitSendFrom({
+ required BuildContext context,
+ required CryptoCurrency coin,
+ required Amount? amount,
+ required String address,
+ required ShopInBitOrderModel model,
+ EthContract? tokenContract,
+ bool popDesktopBeforeShow = false,
+ String? routeOnSuccessName,
+}) {
+ if (Util.isDesktop) {
+ if (popDesktopBeforeShow) {
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+ unawaited(
+ showDialog<void>(
+ context: context,
+ builder: (_) => ShopInBitSendFromView(
+ coin: coin,
+ amount: amount,
+ address: address,
+ model: 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: model,
+ tokenContract: tokenContract,
+ routeOnSuccessName: routeOnSuccessName,
+ ),
+ settings: const RouteSettings(name: ShopInBitSendFromView.routeName),
+ ),
+ );
+ }
+}
+
+// Tries to launch the in-wallet send flow for [ticker]/[address]. Returns
+// true when navigation happened. Returns false when no compatible wallet
+// or token contract was found, leaving the caller to handle the
+// "pay externally" path (flushbar, status change, etc).
+bool tryNavigateToShopInBitWalletSend({
+ required WidgetRef ref,
+ required BuildContext context,
+ required String ticker,
+ required String address,
+ required Amount? amount,
+ required ShopInBitOrderModel model,
+ bool popDesktopBeforeShow = false,
+ String? routeOnSuccessName,
+}) {
+ if (address.isEmpty) return false;
+
+ final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
+ if (coin != null) {
+ _pushShopInBitSendFrom(
+ context: context,
+ coin: coin,
+ amount: amount,
+ address: address,
+ model: model,
+ popDesktopBeforeShow: popDesktopBeforeShow,
+ routeOnSuccessName: routeOnSuccessName,
+ );
+ return true;
+ }
+
+ if (ticker == "USDT") {
+ final tokenContract = ref
+ .read(mainDBProvider)
+ .getEthContractSync(kShopInBitUsdtContractAddress);
+ if (tokenContract != null) {
+ final ethCoin = AppConfig.getCryptoCurrencyForTicker("ETH");
+ if (ethCoin != null) {
+ _pushShopInBitSendFrom(
+ context: context,
+ coin: ethCoin,
+ amount: amount,
+ address: address,
+ model: model,
+ tokenContract: tokenContract,
+ popDesktopBeforeShow: popDesktopBeforeShow,
+ routeOnSuccessName: routeOnSuccessName,
+ );
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// Shared mobile chrome for the two ShopInBit payment views: Background +
+// PopScope (back goes through [onBack]) + AppBar + scrollable, intrinsic
+// height body. Set [showLoading] to overlay a spinner.
+class ShopInBitPaymentMobileScaffold extends StatelessWidget {
+ const ShopInBitPaymentMobileScaffold({
+ super.key,
+ required this.onBack,
+ required this.child,
+ this.showLoading = false,
+ });
+
+ final VoidCallback onBack;
+ final Widget child;
+ final bool showLoading;
+
+ @override
+ Widget build(BuildContext context) {
+ return Background(
+ child: PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (bool didPop, dynamic result) {
+ if (!didPop) {
+ onBack();
+ }
+ },
+ child: Scaffold(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(onPressed: onBack),
+ title: Text("ShopinBit", style: STextStyles.navBarTitle(context)),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Stack(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 32,
+ ),
+ child: IntrinsicHeight(child: child),
+ ),
+ ),
+ ),
+ if (showLoading)
+ const LoadingIndicator(width: 24, height: 24),
+ ],
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart
index 38895fd..f9216cf 100644
--- a/lib/pages/shopinbit/shopinbit_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_payment_view.dart
@@ -1,37 +1,30 @@
import 'dart:async';
import 'dart:io';
-import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../app_config.dart';
-import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/global/shopin_bit_service_provider.dart';
import '../../providers/providers.dart';
-import '../../route_generator.dart';
import '../../services/shopinbit/src/models/payment.dart';
import '../../themes/coin_icon_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/address_utils.dart';
-import '../../utilities/amount/amount.dart';
import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
-import '../../wallets/crypto_currency/crypto_currency.dart';
-import '../../widgets/background.dart';
-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/loading_indicator.dart';
import '../../widgets/rounded_white_container.dart';
-import 'shopinbit_send_from_view.dart';
+import 'shopinbit_payment_shared.dart';
class ShopInBitPaymentView extends ConsumerStatefulWidget {
const ShopInBitPaymentView({super.key, required this.model});
@@ -255,81 +248,25 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
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'];
- }
- }
- if (amountStr == null || amountStr.isEmpty) {
- amountStr = _paymentInfo?.due;
- }
-
- 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 (_) {}
- }
- }
+ final target = parseShopInBitPaymentTarget(
+ paymentUri: _currentAddress,
+ ticker: ticker,
+ coin: AppConfig.getCryptoCurrencyForTicker(ticker),
+ amountFallback: _paymentInfo?.due,
+ );
- if (coin != null && address.isNotEmpty) {
- _navigateToSendFrom(coin: coin, amount: amount, address: address);
+ if (tryNavigateToShopInBitWalletSend(
+ ref: ref,
+ context: context,
+ ticker: ticker,
+ address: target.address,
+ amount: target.amount,
+ model: widget.model,
+ popDesktopBeforeShow: true,
+ )) {
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;
- }
- }
- }
-
widget.model.status = ShopInBitOrderStatus.paymentPending;
widget.model.paymentMethod = method;
@@ -352,64 +289,6 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
}
}
- 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),
- ),
- );
- }
- }
-
- bool _hasWalletForTicker(String ticker) {
- if (ticker == "USDT") {
- const usdtAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
- return ref
- .read(pWallets)
- .wallets
- .any(
- (w) =>
- w.info.coin is Ethereum &&
- w.info.tokenContractAddresses.contains(usdtAddress),
- );
- } else {
- final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
- if (coin != null) {
- return ref.read(pWallets).wallets.any((e) => e.info.coin == coin);
- }
- }
- return false;
- }
-
String? _parseBip21Amount(String bip21Uri) {
final parsed = AddressUtils.parsePaymentUri(bip21Uri);
String? amountStr = parsed?.amount;
@@ -491,12 +370,13 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
+ final wallets = ref.watch(pWallets);
// Build coin rows from _methods/_addresses
final coinRows = <Widget>[];
for (int i = 0; i < _methods.length; i++) {
final ticker = _methods[i].toUpperCase();
final coin = AppConfig.getCryptoCurrencyForTicker(ticker);
- final hasWallet = _hasWalletForTicker(ticker);
+ final hasWallet = hasShopInBitWalletForTicker(wallets, ticker);
final amountStr = _addresses[i].isNotEmpty
? _parseBip21Amount(_addresses[i])
: null;
@@ -759,46 +639,10 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
);
}
- return Background(
- 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)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Stack(
- children: [
- Padding(
- padding: const EdgeInsets.all(16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 32,
- ),
- child: IntrinsicHeight(child: content),
- ),
- ),
- ),
- if (_loading) const LoadingIndicator(width: 24, height: 24),
- ],
- );
- },
- ),
- ),
- ),
- ),
+ return ShopInBitPaymentMobileScaffold(
+ onBack: _popToTickets,
+ showLoading: _loading,
+ child: content,
);
}
}
diff --git a/lib/pages/shopinbit/shopinbit_shipping_view.dart b/lib/pages/shopinbit/shopinbit_shipping_view.dart
index 298e293..597656f 100644
--- a/lib/pages/shopinbit/shopinbit_shipping_view.dart
+++ b/lib/pages/shopinbit/shopinbit_shipping_view.dart
@@ -63,6 +63,10 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
List<Map<String, dynamic>> _countries = [];
String? _selectedCountryIso;
bool _loadingCountries = false;
+ // True when we arrived with a pre-set delivery country (the normal new-order
+ // path). Restored-from-API orders land here with no country, so we unlock
+ // the dropdown only in that case.
+ late final bool _countryLocked;
bool _submitting = false;
@@ -109,6 +113,7 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_selectedCountryIso = widget.model.deliveryCountry.isNotEmpty
? widget.model.deliveryCountry
: null;
+ _countryLocked = _selectedCountryIso != null;
for (final node in [
_nameFocusNode,
@@ -341,9 +346,11 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_countrySearchController.clear();
}
},
- onChanged: null,
+ onChanged: (_countryLocked || _loadingCountries)
+ ? null
+ : (value) => setState(() => _selectedCountryIso = value),
hint: Text(
- "Country",
+ _loadingCountries ? "Loading countries..." : "Country",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
Why this scored 15/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.