feat(shopinbit): add ShopInBit confirm send views
What changed, and why it matters
This commit adds two new user interface screens for a feature called ShopInBit: one for collecting a billing address for a car research fee, and another for confirming and sending a cryptocurrency payment. It also registers the new address screen in the app's route list. There is no indication in the code that this fixes a security bug; it appears to be ordinary feature development.
No security action required; treat as routine feature code. Standard code review for input handling, error handling, and transaction flow correctness is sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff introduces shopinbit_car_fee_view.dart (a form collecting name, street, city, postal code, country and persisting a ShopInBit ticket) and shopinbit_confirm_send_view.dart (a transaction confirmation screen that authenticates the user, calls wallet.confirmSend, saves a transaction note, and updates the order status). route_generator.dart adds the new route. No security-sensitive changes such as input validation bypasses, cryptographic downgrades, or authentication removal are visible. The transaction flow still requires user authentication via DesktopAuthSend or LockscreenView before broadcasting.
Changed components
lib/pages/shopinbit/shopinbit_car_fee_view.dartlib/pages/shopinbit/shopinbit_confirm_send_view.dartlib/route_generator.dartInspect captured patch +998 / −0
diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
new file mode 100644
index 0000000..340b99c
--- /dev/null
+++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
@@ -0,0 +1,300 @@
+import 'package:flutter/material.dart';
+
+import '../../db/isar/main_db.dart';
+import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/constants.dart';
+import '../../utilities/text_styles.dart';
+import '../../utilities/util.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/rounded_white_container.dart';
+import '../../widgets/stack_text_field.dart';
+import 'shopinbit_order_created.dart';
+
+class ShopInBitCarFeeView extends StatefulWidget {
+ const ShopInBitCarFeeView({super.key, required this.model});
+
+ static const String routeName = "/shopInBitCarFee";
+
+ final ShopInBitOrderModel model;
+
+ @override
+ State<ShopInBitCarFeeView> createState() => _ShopInBitCarFeeViewState();
+}
+
+class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
+ late final TextEditingController _nameController;
+ late final TextEditingController _streetController;
+ late final TextEditingController _cityController;
+ late final TextEditingController _postalCodeController;
+ late final TextEditingController _countryController;
+ late final FocusNode _nameFocusNode;
+ late final FocusNode _streetFocusNode;
+ late final FocusNode _cityFocusNode;
+ late final FocusNode _postalCodeFocusNode;
+ late final FocusNode _countryFocusNode;
+
+ bool get _canContinue =>
+ _nameController.text.trim().isNotEmpty &&
+ _streetController.text.trim().isNotEmpty &&
+ _cityController.text.trim().isNotEmpty &&
+ _postalCodeController.text.trim().isNotEmpty &&
+ _countryController.text.trim().isNotEmpty;
+
+ @override
+ void initState() {
+ super.initState();
+ _nameController = TextEditingController();
+ _streetController = TextEditingController();
+ _cityController = TextEditingController();
+ _postalCodeController = TextEditingController();
+ _countryController = TextEditingController();
+ _nameFocusNode = FocusNode();
+ _streetFocusNode = FocusNode();
+ _cityFocusNode = FocusNode();
+ _postalCodeFocusNode = FocusNode();
+ _countryFocusNode = FocusNode();
+
+ for (final node in [
+ _nameFocusNode,
+ _streetFocusNode,
+ _cityFocusNode,
+ _postalCodeFocusNode,
+ _countryFocusNode,
+ ]) {
+ node.addListener(() => setState(() {}));
+ }
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _streetController.dispose();
+ _cityController.dispose();
+ _postalCodeController.dispose();
+ _countryController.dispose();
+ _nameFocusNode.dispose();
+ _streetFocusNode.dispose();
+ _cityFocusNode.dispose();
+ _postalCodeFocusNode.dispose();
+ _countryFocusNode.dispose();
+ super.dispose();
+ }
+
+ void _payFee() {
+ widget.model.ticketId =
+ "SIB-${DateTime.now().millisecondsSinceEpoch % 10000}";
+ widget.model.status = ShopInBitOrderStatus.pending;
+ MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ showDialog<void>(
+ context: context,
+ builder: (_) => ShopInBitOrderCreated(model: widget.model),
+ );
+ } else {
+ Navigator.of(
+ context,
+ ).pushNamed(ShopInBitOrderCreated.routeName, arguments: widget.model);
+ }
+ }
+
+ Widget _buildField({
+ required TextEditingController controller,
+ required FocusNode focusNode,
+ required String label,
+ required bool isDesktop,
+ }) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ autocorrect: false,
+ enableSuggestions: false,
+ onChanged: (_) => setState(() {}),
+ style: isDesktop
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ )
+ : STextStyles.field(context),
+ decoration:
+ standardInputDecoration(
+ label,
+ focusNode,
+ context,
+ desktopMed: isDesktop,
+ ).copyWith(
+ filled: true,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isDesktop = Util.isDesktop;
+ final spacing = SizedBox(height: isDesktop ? 16 : 12);
+
+ final content = Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ "Car research fee",
+ style: isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ SizedBox(height: isDesktop ? 16 : 8),
+ RoundedWhiteContainer(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Research fee",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.itemSubtitle(context),
+ ),
+ Text(
+ "50.00 EUR",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.itemSubtitle(context),
+ ),
+ ],
+ ),
+ ),
+ SizedBox(height: isDesktop ? 24 : 16),
+ Text(
+ "Billing address",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.titleBold12(context),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _buildField(
+ controller: _nameController,
+ focusNode: _nameFocusNode,
+ label: "Full name",
+ isDesktop: isDesktop,
+ ),
+ spacing,
+ _buildField(
+ controller: _streetController,
+ focusNode: _streetFocusNode,
+ label: "Street address",
+ isDesktop: isDesktop,
+ ),
+ spacing,
+ Row(
+ children: [
+ Expanded(
+ child: _buildField(
+ controller: _cityController,
+ focusNode: _cityFocusNode,
+ label: "City",
+ isDesktop: isDesktop,
+ ),
+ ),
+ SizedBox(width: isDesktop ? 16 : 12),
+ Expanded(
+ child: _buildField(
+ controller: _postalCodeController,
+ focusNode: _postalCodeFocusNode,
+ label: "Postal code",
+ isDesktop: isDesktop,
+ ),
+ ),
+ ],
+ ),
+ spacing,
+ _buildField(
+ controller: _countryController,
+ focusNode: _countryFocusNode,
+ label: "Country",
+ isDesktop: isDesktop,
+ ),
+ const Spacer(),
+ PrimaryButton(
+ label: "Pay research fee",
+ enabled: _canContinue,
+ onPressed: _canContinue ? _payFee : null,
+ ),
+ ],
+ );
+
+ if (isDesktop) {
+ return DesktopDialog(
+ maxWidth: 580,
+ maxHeight: 650,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "ShopInBit",
+ style: STextStyles.desktopH3(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ child: content,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ 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),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/shopinbit/shopinbit_confirm_send_view.dart b/lib/pages/shopinbit/shopinbit_confirm_send_view.dart
new file mode 100644
index 0000000..47b13d3
--- /dev/null
+++ b/lib/pages/shopinbit/shopinbit_confirm_send_view.dart
@@ -0,0 +1,687 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../models/isar/models/isar_models.dart';
+import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../notifications/show_flush_bar.dart';
+import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart';
+import '../../providers/providers.dart';
+import '../../route_generator.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/amount/amount.dart';
+import '../../utilities/amount/amount_formatter.dart';
+import '../../utilities/constants.dart';
+import '../../utilities/logger.dart';
+import '../../utilities/text_styles.dart';
+import '../../utilities/util.dart';
+import '../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../wallets/models/tx_data.dart';
+import '../../widgets/background.dart';
+import '../../widgets/conditional_parent.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/rounded_container.dart';
+import '../../widgets/rounded_white_container.dart';
+import '../../widgets/stack_dialog.dart';
+import '../pinpad_views/lock_screen_view.dart';
+import '../send_view/sub_widgets/sending_transaction_dialog.dart';
+import '../wallet_view/wallet_view.dart';
+
+class ShopInBitConfirmSendView extends ConsumerStatefulWidget {
+ const ShopInBitConfirmSendView({
+ super.key,
+ required this.txData,
+ required this.walletId,
+ this.routeOnSuccessName = WalletView.routeName,
+ required this.model,
+ });
+
+ static const String routeName = "/shopInBitConfirmSend";
+
+ final TxData txData;
+ final String walletId;
+ final String routeOnSuccessName;
+ final ShopInBitOrderModel model;
+
+ @override
+ ConsumerState<ShopInBitConfirmSendView> createState() =>
+ _ShopInBitConfirmSendViewState();
+}
+
+class _ShopInBitConfirmSendViewState
+ extends ConsumerState<ShopInBitConfirmSendView> {
+ late final String walletId;
+ late final String routeOnSuccessName;
+ late final ShopInBitOrderModel model;
+
+ final isDesktop = Util.isDesktop;
+
+ Future<void> _attemptSend(BuildContext context) async {
+ final wallet = ref.read(pWallets).getWallet(walletId);
+ final coin = wallet.info.coin;
+
+ final sendProgressController = ProgressAndSuccessController();
+
+ unawaited(
+ showDialog<void>(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: false,
+ builder: (context) {
+ return SendingTransactionDialog(
+ coin: coin,
+ controller: sendProgressController,
+ );
+ },
+ ),
+ );
+
+ final time = Future<dynamic>.delayed(const Duration(milliseconds: 2500));
+
+ late String txid;
+ Future<TxData> txidFuture;
+
+ final String note = widget.txData.note ?? "";
+
+ try {
+ txidFuture = wallet.confirmSend(txData: widget.txData);
+
+ unawaited(wallet.refresh());
+
+ final results = await Future.wait([txidFuture, time]);
+
+ sendProgressController.triggerSuccess?.call();
+ await Future<void>.delayed(const Duration(seconds: 5));
+
+ txid = (results.first as TxData).txid!;
+
+ // save note
+ await ref
+ .read(mainDBProvider)
+ .putTransactionNote(
+ TransactionNote(walletId: walletId, txid: txid, value: note),
+ );
+
+ // Update model status after successful broadcast
+ model.status = ShopInBitOrderStatus.paymentPending;
+ model.paymentMethod = coin.ticker.toUpperCase();
+
+ // pop back to wallet
+ if (context.mounted) {
+ if (Util.isDesktop) {
+ // pop sending dialog
+ Navigator.of(context, rootNavigator: true).pop();
+
+ // one day we'll do routing right
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+
+ Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
+ }
+ } catch (e, s) {
+ Logging.instance.e(
+ "Broadcast transaction failed: ",
+ error: e,
+ stackTrace: s,
+ );
+
+ // pop sending dialog
+ Navigator.of(context).pop();
+
+ await showDialog<dynamic>(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return StackDialog(
+ title: "Broadcast transaction failed",
+ message: e.toString(),
+ rightButton: TextButton(
+ style: Theme.of(context)
+ .extension<StackColors>()!
+ .getSecondaryEnabledButtonStyle(context),
+ child: Text(
+ "Ok",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.buttonTextSecondary,
+ ),
+ ),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ );
+ },
+ );
+ }
+ }
+
+ Future<void> _confirmSend() async {
+ final dynamic unlocked;
+
+ final coin = ref.read(pWalletCoin(walletId));
+
+ if (Util.isDesktop) {
+ unlocked = await showDialog<bool?>(
+ context: context,
+ builder: (context) => DesktopDialog(
+ maxWidth: 580,
+ maxHeight: double.infinity,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [DesktopDialogCloseButton()],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32),
+ child: DesktopAuthSend(coin: coin),
+ ),
+ ],
+ ),
+ ),
+ );
+ } else {
+ unlocked = await Navigator.push(
+ context,
+ RouteGenerator.getRoute<dynamic>(
+ shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
+ builder: (_) => const LockscreenView(
+ showBackButton: true,
+ popOnSuccess: true,
+ routeOnSuccessArguments: true,
+ routeOnSuccess: "",
+ biometricsCancelButtonString: "CANCEL",
+ biometricsLocalizedReason: "Authenticate to send transaction",
+ biometricsAuthenticationTitle: "Confirm Transaction",
+ ),
+ settings: const RouteSettings(name: "/confirmsendlockscreen"),
+ ),
+ );
+ }
+
+ if (unlocked is bool && mounted) {
+ if (unlocked) {
+ await _attemptSend(context);
+ } else {
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: "Invalid passphrase",
+ context: context,
+ ),
+ );
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ walletId = widget.walletId;
+ routeOnSuccessName = widget.routeOnSuccessName;
+ model = widget.model;
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ConditionalParent(
+ condition: !isDesktop,
+ builder: (child) {
+ return Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ appBar: AppBar(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.backgroundAppBar,
+ leading: AppBarBackButton(
+ onPressed: () async {
+ Navigator.of(context).pop();
+ },
+ ),
+ title: Text(
+ "Confirm transaction",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (builderContext, constraints) {
+ return Padding(
+ padding: const EdgeInsets.only(
+ left: 12,
+ top: 12,
+ right: 12,
+ ),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 24,
+ ),
+ child: IntrinsicHeight(
+ child: Padding(
+ padding: const EdgeInsets.all(4),
+ child: child,
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ },
+ child: ConditionalParent(
+ condition: isDesktop,
+ builder: (child) => DesktopDialog(
+ maxHeight: double.infinity,
+ maxWidth: 580,
+ child: Column(
+ children: [
+ Row(
+ children: [
+ const SizedBox(width: 6),
+ const AppBarBackButton(isCompact: true, iconSize: 23),
+ const SizedBox(width: 12),
+ Text(
+ "Confirm ${ref.watch(pWalletCoin(walletId)).ticker} transaction",
+ style: STextStyles.desktopH3(context),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32),
+ child: Column(
+ children: [
+ RoundedWhiteContainer(
+ padding: const EdgeInsets.all(0),
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ child: child,
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Text(
+ "Transaction fee",
+ style: STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ RoundedContainer(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(
+ ref
+ .watch(
+ pAmountFormatter(
+ ref.watch(pWalletCoin(walletId)),
+ ),
+ )
+ .format(widget.txData.fee!),
+ style:
+ STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ RoundedContainer(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.snackBarBackSuccess,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Total amount",
+ style: STextStyles.titleBold12(context).copyWith(
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textConfirmTotalAmount,
+ ),
+ ),
+ Builder(
+ builder: (context) {
+ final coin = ref.read(pWalletCoin(walletId));
+ final fee = widget.txData.fee!;
+ final amount = widget.txData.amountWithoutChange!;
+ final total = amount + fee;
+
+ return Text(
+ ref.watch(pAmountFormatter(coin)).format(total),
+ style: STextStyles.itemSubtitle12(context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textConfirmTotalAmount,
+ ),
+ textAlign: TextAlign.right,
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ buttonHeight: ButtonHeight.l,
+ onPressed: Navigator.of(context).pop,
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: PrimaryButton(
+ label: "Send",
+ buttonHeight: isDesktop ? ButtonHeight.l : null,
+ onPressed: _confirmSend,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ConditionalParent(
+ condition: isDesktop,
+ builder: (child) => Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).extension<StackColors>()!.background,
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(Constants.size.circularBorderRadius),
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Row(children: [child]),
+ ),
+ ),
+ child: Text(
+ "Send ${ref.watch(pWalletCoin(walletId)).ticker}",
+ style: isDesktop
+ ? STextStyles.desktopTextMedium(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text("Send from", style: STextStyles.smallMed12(context)),
+ const SizedBox(height: 4),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ "ShopInBit address",
+ style: STextStyles.smallMed12(context),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ widget.txData.recipients!.first.address,
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text("Amount", style: STextStyles.smallMed12(context)),
+ ConditionalParent(
+ condition: isDesktop,
+ builder: (child) => Row(
+ children: [
+ child,
+ Builder(
+ builder: (context) {
+ final coin = ref.watch(pWalletCoin(walletId));
+ final price = ref.watch(
+ priceAnd24hChangeNotifierProvider.select(
+ (value) => value.getPrice(coin),
+ ),
+ );
+ final String extra;
+ if (price == null) {
+ extra = "";
+ } else {
+ final amountWithoutChange =
+ widget.txData.amountWithoutChange!;
+ final value =
+ (price.value * amountWithoutChange.decimal)
+ .toAmount(fractionDigits: 2);
+ final currency = ref.watch(
+ prefsChangeNotifierProvider.select(
+ (value) => value.currency,
+ ),
+ );
+ final locale = ref.watch(
+ localeServiceChangeNotifierProvider.select(
+ (value) => value.locale,
+ ),
+ );
+
+ extra =
+ " | ${value.fiatString(locale: locale)} $currency";
+ }
+
+ return Text(
+ extra,
+ style:
+ STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle2,
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ child: Text(
+ ref
+ .watch(
+ pAmountFormatter(ref.watch(pWalletCoin(walletId))),
+ )
+ .format(widget.txData.amountWithoutChange!),
+ style: STextStyles.itemSubtitle12(context),
+ textAlign: TextAlign.right,
+ ),
+ ),
+ ],
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Transaction fee",
+ style: STextStyles.smallMed12(context),
+ ),
+ Text(
+ ref
+ .watch(
+ pAmountFormatter(ref.read(pWalletCoin(walletId))),
+ )
+ .format(widget.txData.fee!),
+ style: STextStyles.itemSubtitle12(context),
+ textAlign: TextAlign.right,
+ ),
+ ],
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text("Note", style: STextStyles.smallMed12(context)),
+ const SizedBox(height: 4),
+ Text(
+ widget.txData.note ?? "",
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ ),
+ isDesktop
+ ? Container(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ height: 1,
+ )
+ : const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text("Ticket ID", style: STextStyles.smallMed12(context)),
+ Text(
+ model.ticketId ?? "",
+ style: STextStyles.itemSubtitle12(context),
+ textAlign: TextAlign.right,
+ ),
+ ],
+ ),
+ ),
+ if (!isDesktop) const SizedBox(height: 12),
+ if (!isDesktop)
+ RoundedContainer(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.snackBarBackSuccess,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Total amount",
+ style: STextStyles.titleBold12(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textConfirmTotalAmount,
+ ),
+ ),
+ Builder(
+ builder: (context) {
+ final coin = ref.watch(pWalletCoin(walletId));
+ final fee = widget.txData.fee!;
+ final amount = widget.txData.amountWithoutChange!;
+ final total = amount + fee;
+
+ return Text(
+ ref.watch(pAmountFormatter(coin)).format(total),
+ style: STextStyles.itemSubtitle12(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textConfirmTotalAmount,
+ ),
+ textAlign: TextAlign.right,
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ if (!isDesktop) const SizedBox(height: 16),
+ if (!isDesktop) const Spacer(),
+ if (!isDesktop)
+ PrimaryButton(
+ label: "Send",
+ buttonHeight: isDesktop ? ButtonHeight.l : null,
+ onPressed: _confirmSend,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index 240414e..f25879b 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -88,6 +88,7 @@ import 'pages/more_view/services_view.dart';
import 'pages/shopinbit/shopinbit_step_1.dart';
import 'pages/shopinbit/shopinbit_step_2.dart';
import 'pages/shopinbit/shopinbit_step_3.dart';
+import 'pages/shopinbit/shopinbit_car_fee_view.dart';
import 'pages/shopinbit/shopinbit_offer_view.dart';
import 'pages/shopinbit/shopinbit_shipping_view.dart';
import 'pages/shopinbit/shopinbit_step_4.dart';
@@ -1144,6 +1145,16 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
+ case ShopInBitCarFeeView.routeName:
+ if (args is ShopInBitOrderModel) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => ShopInBitCarFeeView(model: args),
+ settings: RouteSettings(name: settings.name),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case GlobalSettingsView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
Why this scored 11/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.