What changed, and why it matters
This commit is a routine UI cleanup for work-in-progress gift-card and shopping features. It swaps custom text fields and dialogs for shared reusable widgets, removes unused focus nodes, and fixes small layout and error-message details. There is no clear security problem in the changes, and nothing in the commit message or diff indicates a security fix.
No security action required. Treat as normal code-quality/UI refactor. If reviewing further, verify that AdaptiveTextField and SDialog do not introduce input-validation regressions compared with the replaced widgets.
Security signals we found
No security-relevant keywords in commit title or message
No changes to authentication, cryptography, networking, or storage
Refactoring only: widget replacement, layout cleanup, dead-code removal
Error-message improvement exposes server response body to user dialog (information disclosure surface slightly increased, but only to the local user)
Evidence from the diff
The diff refactors several Dart files in the Stack Wallet Flutter app. Key changes: replace bespoke TextField/FocusNode implementations with a new AdaptiveTextField widget; replace DesktopDialog wrappers with a shared SDialog widget; move _canPurchase from a getter to a state variable with _updateCanPurchase(); improve error message formatting in CakePay order creation by including responseBody; remove unused imports and focus-node disposal; adjust denomination display precision from 0 to 2 decimals; add SingleChildScrollView and layout tweaks. No cryptographic, authentication, network, or data-handling logic is modified. No security relevance is stated by the vendor.
Changed components
lib/pages/cakepay/cakepay_card_detail_view.dartlib/pages/shopinbit/shopinbit_step_1.dartlib/pages/shopinbit/shopinbit_step_2.dartlib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dartlib/widgets/dialogs/s_dialog.dartlib/widgets/textfields/adaptive_text_field.dartInspect captured patch +494 / −624
diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart
index 371d61e..07ec3d6 100644
--- a/lib/pages/cakepay/cakepay_card_detail_view.dart
+++ b/lib/pages/cakepay/cakepay_card_detail_view.dart
@@ -6,7 +6,6 @@ import 'package:url_launcher/url_launcher.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
import '../../themes/stack_colors.dart';
-import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../widgets/background.dart';
@@ -16,10 +15,11 @@ 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/dialogs/s_dialog.dart';
import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
-import '../../widgets/stack_text_field.dart';
+import '../../widgets/textfields/adaptive_text_field.dart';
import 'cakepay_order_view.dart';
class CakePayCardDetailView extends StatefulWidget {
@@ -40,29 +40,17 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
int _quantity = 1;
bool _termsAccepted = false;
final _customAmountController = TextEditingController();
- final _customAmountFocusNode = FocusNode();
final _emailController = TextEditingController();
- final _emailFocusNode = FocusNode();
- @override
- void initState() {
- super.initState();
- _card = widget.card;
- if (_card.isFixedDenomination && _card.denominations.isNotEmpty) {
- _selectedDenomination = _card.denominations.first;
- }
- _emailFocusNode.addListener(() {
- setState(() {});
- });
- }
+ bool _canPurchase = false;
- @override
- void dispose() {
- _customAmountController.dispose();
- _customAmountFocusNode.dispose();
- _emailController.dispose();
- _emailFocusNode.dispose();
- super.dispose();
+ void _updateCanPurchase() {
+ if (mounted) {
+ final check = _checkCanPurchase();
+ if (check != _canPurchase) {
+ setState(() => _canPurchase = check);
+ }
+ }
}
String get _priceString {
@@ -72,7 +60,7 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
return _customAmountController.text.trim();
}
- bool get _canPurchase {
+ bool _checkCanPurchase() {
if (!_termsAccepted || _purchasing) return false;
if (_emailController.text.trim().isEmpty) return false;
final price = _priceString;
@@ -184,7 +172,7 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
}
Future<void> _purchase() async {
- if (!_canPurchase) return;
+ if (!_checkCanPurchase()) return;
setState(() => _purchasing = true);
final resp = await CakePayService.instance.client.createOrder(
@@ -202,8 +190,6 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
if (!resp.hasError && resp.value != null) {
final order = resp.value!;
- // Track order ID locally so the orders list view can fetch it
- // via getOrder() without requiring Knox user auth.
await CakePayService.instance.addOrderId(order.orderId);
if (mounted) {
@@ -221,28 +207,24 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
}
}
} else {
+ final String errorMessage;
+ if (resp.exception != null) {
+ final ex = resp.exception!;
+ final body = ex.responseBody;
+ errorMessage = "${ex.message}${body != null ? "\n$body" : ""}";
+ } else {
+ errorMessage = "Failed to create order";
+ }
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
- return StackDialog(
+ return StackOkDialog(
title: "Purchase failed",
- message: resp.exception?.message ?? "Failed to create order",
- 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(),
- ),
+ message: errorMessage,
+ maxWidth: Util.isDesktop ? 580 : null,
+ desktopPopRootNavigator: Util.isDesktop,
);
},
);
@@ -250,6 +232,22 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
}
}
+ @override
+ void initState() {
+ super.initState();
+ _card = widget.card;
+ if (_card.isFixedDenomination && _card.denominations.isNotEmpty) {
+ _selectedDenomination = _card.denominations.first;
+ }
+ }
+
+ @override
+ void dispose() {
+ _customAmountController.dispose();
+ _emailController.dispose();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
@@ -257,34 +255,33 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
return ConditionalParent(
condition: isDesktop,
- builder: (child) => DesktopDialog(
- maxWidth: 580,
- maxHeight: 700,
- child: Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Gift Card",
- style: STextStyles.desktopH3(context),
+ builder: (child) => SDialog(
+ child: SizedBox(
+ width: 580,
+ child: Column(
+ mainAxisSize: .min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Gift Card",
+ style: STextStyles.desktopH3(context),
+ ),
),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Flexible(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: child,
),
- const DesktopDialogCloseButton(),
- ],
- ),
- Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 8,
- ),
- child: child,
),
- ),
- ],
+ ],
+ ),
),
),
child: ConditionalParent(
@@ -303,105 +300,108 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
- child: child,
+ child: SingleChildScrollView(child: child),
),
),
),
),
- child: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- if (card.cardImageUrl != null)
- _CardImage(imageUrl: card.cardImageUrl!, isDesktop: isDesktop),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: .min,
+ children: [
+ if (card.cardImageUrl != null)
+ _CardImage(imageUrl: card.cardImageUrl!, isDesktop: isDesktop),
+ SizedBox(height: isDesktop ? 24 : 16),
+ Text(
+ card.name,
+ style: isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ if (card.description != null && card.description!.isNotEmpty) ...[
SizedBox(height: isDesktop ? 16 : 12),
- Text(
- card.name,
- style: isDesktop
- ? STextStyles.desktopH2(context)
- : STextStyles.pageTitleH1(context),
- ),
- if (card.description != null && card.description!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- _PlainInfoBlock(text: card.description!, isDesktop: isDesktop),
- ],
- if (card.howToUse != null && card.howToUse!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- _TitledInfoBlock(
- title: "How to use",
- body: card.howToUse!,
- isDesktop: isDesktop,
- ),
- ],
- if (card.termsAndConditions != null &&
- card.termsAndConditions!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- _TitledInfoBlock(
- title: "Terms & conditions",
- body: card.termsAndConditions!,
- isDesktop: isDesktop,
- ),
- ],
- if (card.expiryAndValidity != null &&
- card.expiryAndValidity!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- _TitledInfoBlock(
- title: "Expiry & validity",
- body: card.expiryAndValidity!,
- isDesktop: isDesktop,
- ),
- ],
- SizedBox(height: isDesktop ? 24 : 16),
- _DenominationSelector(
- card: card,
- isDesktop: isDesktop,
- selectedDenomination: _selectedDenomination,
- customAmountController: _customAmountController,
- customAmountFocusNode: _customAmountFocusNode,
- onDenominationSelected: (Decimal d) =>
- setState(() => _selectedDenomination = d),
- onCustomAmountChanged: () => setState(() {}),
- ),
+ _PlainInfoBlock(text: card.description!, isDesktop: isDesktop),
+ ],
+ if (card.howToUse != null && card.howToUse!.isNotEmpty) ...[
SizedBox(height: isDesktop ? 16 : 12),
- _QuantityRow(
+ _TitledInfoBlock(
+ title: "How to use",
+ body: card.howToUse!,
isDesktop: isDesktop,
- quantity: _quantity,
- onDecrement: _quantity > 1
- ? () => setState(() => _quantity--)
- : null,
- onIncrement: () => setState(() => _quantity++),
),
+ ],
+ if (card.termsAndConditions != null &&
+ card.termsAndConditions!.isNotEmpty) ...[
SizedBox(height: isDesktop ? 16 : 12),
- _TermsCheckbox(
+ _TitledInfoBlock(
+ title: "Terms & conditions",
+ body: card.termsAndConditions!,
isDesktop: isDesktop,
- accepted: _termsAccepted,
- onToggle: () =>
- setState(() => _termsAccepted = !_termsAccepted),
- onOpenTerms: _openTerms,
),
+ ],
+ if (card.expiryAndValidity != null &&
+ card.expiryAndValidity!.isNotEmpty) ...[
SizedBox(height: isDesktop ? 16 : 12),
- Text(
- "Email for receipt and delivery",
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- const SizedBox(height: 8),
- _EmailField(
+ _TitledInfoBlock(
+ title: "Expiry & validity",
+ body: card.expiryAndValidity!,
isDesktop: isDesktop,
- controller: _emailController,
- focusNode: _emailFocusNode,
- onChanged: () => setState(() {}),
- ),
- SizedBox(height: isDesktop ? 24 : 16),
- PrimaryButton(
- label: _purchasing ? "Processing..." : "Purchase",
- enabled: _canPurchase,
- onPressed: _canPurchase ? _purchase : null,
),
- if (!isDesktop) const SizedBox(height: 16),
],
- ),
+ SizedBox(height: isDesktop ? 24 : 16),
+ _DenominationSelector(
+ card: card,
+ isDesktop: isDesktop,
+ selectedDenomination: _selectedDenomination,
+ customAmountController: _customAmountController,
+ onDenominationSelected: (Decimal d) {
+ setState(() => _selectedDenomination = d);
+ _updateCanPurchase();
+ },
+ onCustomAmountChanged: _updateCanPurchase,
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _QuantityRow(
+ isDesktop: isDesktop,
+ quantity: _quantity,
+ onDecrement: _quantity > 1
+ ? () => setState(() => _quantity--)
+ : null,
+ onIncrement: () => setState(() => _quantity++),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _TermsCheckbox(
+ isDesktop: isDesktop,
+ accepted: _termsAccepted,
+ onToggle: () {
+ setState(() => _termsAccepted = !_termsAccepted);
+ _updateCanPurchase();
+ },
+ onOpenTerms: _openTerms,
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ Text(
+ "Email for receipt and delivery",
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(context),
+ ),
+ const SizedBox(height: 8),
+ AdaptiveTextField(
+ labelText: "Email",
+ controller: _emailController,
+ showPasteClearButton: true,
+ keyboardType: .emailAddress,
+ onChangedComprehensive: (_) => _updateCanPurchase(),
+ ),
+ SizedBox(height: isDesktop ? 24 : 16),
+ PrimaryButton(
+ label: _purchasing ? "Processing..." : "Purchase",
+ enabled: _canPurchase,
+ onPressed: _canPurchase ? _purchase : null,
+ ),
+ SizedBox(height: isDesktop ? 32 : 16),
+ ],
),
),
);
@@ -495,7 +495,6 @@ class _DenominationSelector extends StatelessWidget {
required this.isDesktop,
required this.selectedDenomination,
required this.customAmountController,
- required this.customAmountFocusNode,
required this.onDenominationSelected,
required this.onCustomAmountChanged,
});
@@ -504,7 +503,6 @@ class _DenominationSelector extends StatelessWidget {
final bool isDesktop;
final Decimal? selectedDenomination;
final TextEditingController customAmountController;
- final FocusNode customAmountFocusNode;
final ValueChanged<Decimal> onDenominationSelected;
final VoidCallback onCustomAmountChanged;
@@ -518,7 +516,7 @@ class _DenominationSelector extends StatelessWidget {
final bool selected = d == selectedDenomination;
return ChoiceChip(
label: Text(
- "${d.toStringAsFixed(0)} ${card.currencyCode ?? ''}",
+ "${d.toStringAsFixed(2)} ${card.currencyCode ?? ''}",
style:
(isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
@@ -543,53 +541,22 @@ class _DenominationSelector extends StatelessWidget {
if (card.isRangeDenomination) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: .min,
children: [
Text(
- "Enter amount (${card.minValue?.toStringAsFixed(0) ?? '?'} - "
- "${card.maxValue?.toStringAsFixed(0) ?? '?'} "
+ "Enter amount (${card.minValue?.toStringAsFixed(2) ?? '?'} - "
+ "${card.maxValue?.toStringAsFixed(2) ?? '?'} "
"${card.currencyCode ?? ''})",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.itemSubtitle12(context),
),
const SizedBox(height: 8),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: TextField(
- controller: customAmountController,
- focusNode: customAmountFocusNode,
- keyboardType: const TextInputType.numberWithOptions(
- decimal: true,
- ),
- onChanged: (_) => onCustomAmountChanged(),
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- height: 1.8,
- )
- : STextStyles.field(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- ),
- decoration:
- standardInputDecoration(
- "Amount",
- customAmountFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
+ AdaptiveTextField(
+ labelText: "Amount",
+ controller: customAmountController,
+ keyboardType: const .numberWithOptions(decimal: true),
+ onChangedComprehensive: (_) => onCustomAmountChanged(),
),
],
);
@@ -709,57 +676,3 @@ class _TermsCheckbox extends StatelessWidget {
);
}
}
-
-class _EmailField extends StatelessWidget {
- const _EmailField({
- required this.isDesktop,
- required this.controller,
- required this.focusNode,
- required this.onChanged,
- });
-
- final bool isDesktop;
- final TextEditingController controller;
- final FocusNode focusNode;
- final VoidCallback onChanged;
-
- @override
- Widget build(BuildContext context) {
- return ClipRRect(
- borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
- child: TextField(
- controller: controller,
- focusNode: focusNode,
- autocorrect: false,
- enableSuggestions: false,
- keyboardType: TextInputType.emailAddress,
- onChanged: (_) => onChanged(),
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- height: 1.8,
- )
- : STextStyles.field(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- ),
- decoration:
- standardInputDecoration(
- "Email",
- focusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/shopinbit/shopinbit_step_1.dart b/lib/pages/shopinbit/shopinbit_step_1.dart
index 6e6a097..9a975ae 100644
--- a/lib/pages/shopinbit/shopinbit_step_1.dart
+++ b/lib/pages/shopinbit/shopinbit_step_1.dart
@@ -2,15 +2,15 @@ import 'package:flutter/material.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/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/stack_text_field.dart';
+import '../../widgets/dialogs/s_dialog.dart';
+import '../../widgets/textfields/adaptive_text_field.dart';
import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_2.dart';
@@ -27,27 +27,8 @@ class ShopInBitStep1 extends StatefulWidget {
class _ShopInBitStep1State extends State<ShopInBitStep1> {
late final TextEditingController _nameController;
- late final FocusNode _nameFocusNode;
- bool get _canContinue => _nameController.text.trim().isNotEmpty;
-
- @override
- void initState() {
- super.initState();
- _nameController = TextEditingController(text: widget.model.displayName);
- _nameFocusNode = FocusNode();
-
- _nameFocusNode.addListener(() {
- setState(() {});
- });
- }
-
- @override
- void dispose() {
- _nameController.dispose();
- _nameFocusNode.dispose();
- super.dispose();
- }
+ bool _canContinue = false;
void _continue() {
widget.model.displayName = _nameController.text.trim();
@@ -65,135 +46,130 @@ class _ShopInBitStep1State extends State<ShopInBitStep1> {
}
}
+ @override
+ void initState() {
+ super.initState();
+ _canContinue = widget.model.displayName.isNotEmpty;
+ _nameController = TextEditingController(text: widget.model.displayName);
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
- final content = Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- if (!isDesktop)
- StepRow(
- count: 4,
- current: 0,
- width: MediaQuery.of(context).size.width - 32,
- ),
- if (!isDesktop) const SizedBox(height: 14),
- Text(
- "Create your profile",
- style: isDesktop
- ? STextStyles.desktopH2(context)
- : STextStyles.pageTitleH1(context),
- ),
- SizedBox(height: isDesktop ? 16 : 8),
- Text(
- "Enter a display name to use with ShopinBit.",
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.itemSubtitle(context),
- ),
- SizedBox(height: isDesktop ? 32 : 24),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: TextField(
- controller: _nameController,
- focusNode: _nameFocusNode,
- 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(
- "Display name",
- _nameFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
+ return ConditionalParent(
+ condition: isDesktop,
+ builder: (child) => SDialog(
+ child: SizedBox(
+ width: 580,
+ child: Column(
+ mainAxisSize: .min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "ShopinBit",
+ style: STextStyles.desktopH3(context),
+ ),
),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Flexible(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: child,
),
+ ),
+ ],
),
),
- const Spacer(),
- PrimaryButton(
- label: "Next",
- enabled: _canContinue,
- onPressed: _canContinue ? _continue : null,
+ ),
+ child: ConditionalParent(
+ condition: !isDesktop,
+ builder: (child) => 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: child),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
),
- ],
- );
-
- if (isDesktop) {
- return DesktopDialog(
- maxWidth: 580,
- maxHeight: 400,
child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
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,
+ if (!isDesktop)
+ StepRow(
+ count: 4,
+ current: 0,
+ width: MediaQuery.of(context).size.width - 32,
),
+ const SizedBox(height: 14),
+ Text(
+ "Create your profile",
+ style: isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ SizedBox(height: isDesktop ? 16 : 8),
+ Text(
+ "Enter a display name to use with ShopinBit.",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.itemSubtitle(context),
+ ),
+ SizedBox(height: isDesktop ? 32 : 24),
+ AdaptiveTextField(
+ labelText: "Display name",
+ controller: _nameController,
+ autocorrect: false,
+ enableSuggestions: false,
+ onChangedComprehensive: (value) {
+ if (mounted && _canContinue != value.isNotEmpty) {
+ setState(() => _canContinue = value.isNotEmpty);
+ }
+ },
+ ),
+ isDesktop ? const SizedBox(height: 32) : const Spacer(),
+ PrimaryButton(
+ label: "Next",
+ enabled: _canContinue,
+ onPressed: _canContinue ? _continue : null,
),
+ if (isDesktop) const SizedBox(height: 32),
],
),
- );
- }
-
- 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_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart
index 9df909e..79d6a20 100644
--- a/lib/pages/shopinbit/shopinbit_step_2.dart
+++ b/lib/pages/shopinbit/shopinbit_step_2.dart
@@ -8,10 +8,12 @@ import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.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/dialogs/s_dialog.dart';
+import '../../widgets/rounded_container.dart';
import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_1.dart';
import 'shopinbit_step_3.dart';
@@ -31,14 +33,6 @@ class ShopInBitStep2 extends StatefulWidget {
class _ShopInBitStep2State extends State<ShopInBitStep2> {
ShopInBitCategory? _selected;
- @override
- void initState() {
- super.initState();
- // Reset category selection.
- widget.model.category = null;
- _selected = null;
- }
-
void _popBack() {
if (Util.isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
@@ -82,214 +76,232 @@ class _ShopInBitStep2State extends State<ShopInBitStep2> {
}
}
- Widget _categoryCard({
- required ShopInBitCategory category,
- required String title,
- required String description,
- required String iconAsset,
- required bool isDesktop,
- }) {
- final isSelected = _selected == category;
- return GestureDetector(
- onTap: () => setState(() => _selected = category),
- child: Container(
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(isDesktop ? 16 : 12),
- border: Border.all(
- color: isSelected
- ? Theme.of(context).extension<StackColors>()!.textDark
- : Theme.of(context).extension<StackColors>()!.background,
- width: 2,
+ @override
+ void initState() {
+ super.initState();
+ // Reset category selection.
+ widget.model.category = null;
+ _selected = null;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isDesktop = Util.isDesktop;
+
+ return ConditionalParent(
+ condition: isDesktop,
+ builder: (content) => SDialog(
+ child: SizedBox(
+ width: 580,
+ child: Column(
+ mainAxisSize: .min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ AppBarBackButton(
+ isCompact: true,
+ iconSize: 23,
+ onPressed: _popBack,
+ ),
+ Text("ShopinBit", style: STextStyles.desktopH3(context)),
+ ],
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Flexible(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: content,
+ ),
+ ),
+ ],
),
- color: Theme.of(context).extension<StackColors>()!.popupBG,
),
- padding: EdgeInsets.all(isDesktop ? 20 : 16),
- child: Row(
- children: [
- Container(
- width: isDesktop ? 48 : 40,
- height: isDesktop ? 48 : 40,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textDark.withOpacity(0.1),
- ),
- alignment: Alignment.center,
- child: SvgPicture.asset(
- iconAsset,
- width: isDesktop ? 24 : 20,
- height: isDesktop ? 24 : 20,
- color: Theme.of(context).extension<StackColors>()!.textDark,
+ ),
+ child: ConditionalParent(
+ condition: !isDesktop,
+ builder: (content) => Background(
+ 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),
+ ),
),
- ),
- SizedBox(width: isDesktop ? 16 : 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- title,
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 4),
- Text(
- description,
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ 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),
+ ),
+ ),
+ );
+ },
+ ),
),
),
- if (isSelected)
- Icon(
- Icons.check_circle,
- color: Theme.of(context).extension<StackColors>()!.textDark,
- size: isDesktop ? 24 : 20,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (!isDesktop)
+ StepRow(
+ count: 4,
+ current: 1,
+ width: MediaQuery.of(context).size.width - 32,
),
+ const SizedBox(height: 14),
+ Text(
+ "Choose a service",
+ style: isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ SizedBox(height: isDesktop ? 16 : 8),
+ Text(
+ "Select the type of service you need.",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.itemSubtitle(context),
+ ),
+ SizedBox(height: isDesktop ? 32 : 24),
+ _CategoryCard(
+ category: .concierge,
+ title: "Concierge",
+ description: "Purchase products and services online.",
+ iconAsset: Assets.svg.dollarSign,
+ isSelected: _selected == .concierge,
+ onTap: (value) => setState(() => _selected = value),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _CategoryCard(
+ category: .travel,
+ title: "Travel",
+ description: "Book flights, hotels, and more.",
+ iconAsset: Assets.svg.circleArrowUpRight,
+ isSelected: _selected == .travel,
+ onTap: (value) => setState(() => _selected = value),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _CategoryCard(
+ category: .car,
+ title: "Car",
+ description: "Find and purchase vehicles.",
+ iconAsset: Assets.svg.boxAuto,
+ isSelected: _selected == .car,
+ onTap: (value) => setState(() => _selected = value),
+ ),
+ isDesktop ? const SizedBox(height: 32) : const Spacer(),
+ PrimaryButton(
+ label: "Next",
+ enabled: _selected != null,
+ onPressed: _selected != null ? _continue : null,
+ ),
+ if (isDesktop) const SizedBox(height: 32),
],
),
),
);
}
+}
+
+class _CategoryCard extends StatelessWidget {
+ const _CategoryCard({
+ super.key,
+ required this.category,
+ required this.title,
+ required this.description,
+ required this.iconAsset,
+ required this.isSelected,
+ required this.onTap,
+ });
+
+ final ShopInBitCategory category;
+ final String title;
+ final String description;
+ final String iconAsset;
+ final bool isSelected;
+ final ValueChanged<ShopInBitCategory> onTap;
@override
Widget build(BuildContext context) {
+ final StackColors colors = Theme.of(context).extension<StackColors>()!;
final isDesktop = Util.isDesktop;
- final content = Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- if (!isDesktop)
- StepRow(
- count: 4,
- current: 1,
- width: MediaQuery.of(context).size.width - 32,
+ return RoundedContainer(
+ color: colors.popupBG,
+ borderColor: colors.textFieldDefaultBG,
+ onPressed: () => onTap(category),
+ child: Row(
+ children: [
+ Container(
+ width: isDesktop ? 48 : 40,
+ height: isDesktop ? 48 : 40,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: colors.textDark.withOpacity(0.1),
+ ),
+ alignment: Alignment.center,
+ child: SvgPicture.asset(
+ iconAsset,
+ width: isDesktop ? 24 : 20,
+ height: isDesktop ? 24 : 20,
+ color: colors.textDark,
+ ),
),
- if (!isDesktop) const SizedBox(height: 14),
- Text(
- "Choose a service",
- style: isDesktop
- ? STextStyles.desktopH2(context)
- : STextStyles.pageTitleH1(context),
- ),
- SizedBox(height: isDesktop ? 16 : 8),
- Text(
- "Select the type of service you need.",
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.itemSubtitle(context),
- ),
- SizedBox(height: isDesktop ? 32 : 24),
- _categoryCard(
- category: ShopInBitCategory.concierge,
- title: "Concierge",
- description: "Purchase products and services online.",
- iconAsset: Assets.svg.dollarSign,
- isDesktop: isDesktop,
- ),
- SizedBox(height: isDesktop ? 16 : 12),
- _categoryCard(
- category: ShopInBitCategory.travel,
- title: "Travel",
- description: "Book flights, hotels, and more.",
- iconAsset: Assets.svg.circleArrowUpRight,
- isDesktop: isDesktop,
- ),
- SizedBox(height: isDesktop ? 16 : 12),
- _categoryCard(
- category: ShopInBitCategory.car,
- title: "Car",
- description: "Find and purchase vehicles.",
- iconAsset: Assets.svg.boxAuto,
- isDesktop: isDesktop,
- ),
- const Spacer(),
- PrimaryButton(
- label: "Next",
- enabled: _selected != null,
- onPressed: _selected != null ? _continue : null,
- ),
- ],
- );
-
- if (isDesktop) {
- return DesktopDialog(
- maxWidth: 580,
- maxHeight: 700,
- child: Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ SizedBox(width: isDesktop ? 16 : 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- AppBarBackButton(
- isCompact: true,
- iconSize: 23,
- onPressed: _popBack,
- ),
- Text("ShopinBit", style: STextStyles.desktopH3(context)),
- ],
+ Text(
+ title,
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.titleBold12(context),
),
- const DesktopDialogCloseButton(),
- ],
- ),
- Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 16,
+ const SizedBox(height: 4),
+ Text(
+ description,
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(
+ context,
+ ).copyWith(color: colors.textSubtitle1),
),
- child: content,
- ),
+ ],
),
- ],
- ),
- );
- }
-
- return Background(
- 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)),
),
- 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),
- ),
- ),
- );
- },
+ if (isSelected)
+ SvgPicture.asset(
+ Assets.svg.checkCircle,
+ width: isDesktop ? 24 : 20,
+ height: isDesktop ? 24 : 20,
+ colorFilter: ColorFilter.mode(colors.textDark, .srcIn),
),
- ),
- ),
+ ],
),
);
}
diff --git a/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart
index e5c9e59..010adc5 100644
--- a/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart
+++ b/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart
@@ -10,7 +10,6 @@ import '../../../db/isar/main_db.dart';
import '../../../models/shopinbit/shopinbit_order_model.dart';
import '../../../notifications/show_flush_bar.dart';
import '../../../pages/shopinbit/shopinbit_step_1.dart';
-import '../../../pages/shopinbit/shopinbit_step_2.dart';
import '../../../pages/shopinbit/shopinbit_tickets_view.dart';
import '../../../providers/desktop/current_desktop_menu_item.dart';
import '../../../services/shopinbit/shopinbit_service.dart';
@@ -89,7 +88,7 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
return shouldContinue ?? false;
}
- void _showShopDialog(BuildContext context) async {
+ Future<void> _showShopDialog(BuildContext context) async {
final service = ShopInBitService.instance;
final model = ShopInBitOrderModel();
bool isFirstRun = false;
@@ -111,12 +110,12 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
}
}
- if (!mounted) return;
+ if (!context.mounted) return;
if (isFirstRun) {
// First run: show service overview then go directly to Step2
// (name was just entered in setup dialog, no need to show Step1 again).
- showDialog<void>(
+ await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => DesktopDialog(
@@ -144,36 +143,7 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
),
const Spacer(),
Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SecondaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Cancel",
- onPressed: () {
- Navigator.of(dialogContext, rootNavigator: true).pop();
- },
- ),
- const SizedBox(width: 20),
- PrimaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Continue",
- onPressed: () async {
- Navigator.of(dialogContext, rootNavigator: true).pop();
- await showDialog<void>(
- context: context,
- barrierDismissible: false,
- builder: (_) => ShopInBitStep2(model: model),
- );
- if (mounted) setState(() {});
- },
- ),
- ],
- ),
- const Spacer(),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SecondaryButton(
width: 200,
@@ -183,7 +153,6 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
Navigator.of(dialogContext, rootNavigator: true).pop();
},
),
- const SizedBox(width: 20),
PrimaryButton(
width: 200,
buttonHeight: ButtonHeight.l,
diff --git a/lib/widgets/dialogs/s_dialog.dart b/lib/widgets/dialogs/s_dialog.dart
index a6b3214..6bf66ec 100644
--- a/lib/widgets/dialogs/s_dialog.dart
+++ b/lib/widgets/dialogs/s_dialog.dart
@@ -29,30 +29,26 @@ class SDialog extends StatelessWidget {
return Padding(
padding: margin ?? EdgeInsets.all(Util.isDesktop ? 32 : 16),
child: Column(
- mainAxisAlignment: mainAxisAlignment ??
+ mainAxisAlignment:
+ mainAxisAlignment ??
(Util.isDesktop ? MainAxisAlignment.center : MainAxisAlignment.end),
crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center,
+ mainAxisSize: .min,
children: [
Flexible(
child: Material(
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
- color: background ??
+ color:
+ background ??
Theme.of(context).extension<StackColors>()!.popupBG,
- borderRadius: BorderRadius.circular(
- 20,
- ),
+ borderRadius: BorderRadius.circular(20),
),
child: ConditionalParent(
condition: contentCanScroll,
- builder: (child) => SingleChildScrollView(
- child: child,
- ),
- child: Padding(
- padding: padding,
- child: child,
- ),
+ builder: (child) => SingleChildScrollView(child: child),
+ child: Padding(padding: padding, child: child),
),
),
),
diff --git a/lib/widgets/textfields/adaptive_text_field.dart b/lib/widgets/textfields/adaptive_text_field.dart
index e57746a..da30057 100644
--- a/lib/widgets/textfields/adaptive_text_field.dart
+++ b/lib/widgets/textfields/adaptive_text_field.dart
@@ -26,6 +26,7 @@ class AdaptiveTextField extends StatefulWidget {
this.minLines,
this.maxLines,
this.showPasteClearButton = false,
+ this.keyboardType,
});
final String? labelText;
@@ -50,6 +51,8 @@ class AdaptiveTextField extends StatefulWidget {
/// If this is not null, [showPasteClearButton] will be ignored.
final List<Widget>? suffixIcons;
+ final TextInputType? keyboardType;
+
@override
State<AdaptiveTextField> createState() => _AdaptiveTextFieldState();
}
@@ -112,6 +115,7 @@ class _AdaptiveTextFieldState extends State<AdaptiveTextField> {
autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions,
onSubmitted: widget.onSubmitted,
+ keyboardType: widget.keyboardType,
decoration:
standardInputDecoration(
widget.labelText,
Why this scored 14/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.