fix(shopinbit): WIP/not fully tested: add state/province where required
What changed, and why it matters
This commit is a work-in-progress feature update for the ShopinBit integration inside Stack Wallet. It adds the ability to collect and send a U.S. state or Canadian province/territory when required for delivery or billing addresses. It is not a security patch; it is a functional change to address form handling and API request data. There is no indication in the commit that it fixes a vulnerability, leak, or exploit.
No security action required. Treat as a normal feature commit. If reviewing for release readiness, note the commit message explicitly marks the change as 'WIP/not fully tested' and verify the state/province logic and UI flows are completed and tested before shipping.
Security signals we found
No security-relevant signals detected in the diff.
Functional feature addition: state/province collection for US/CA addresses.
Debug-only logging added for HTTP responses (kDebugMode guard).
No input validation, injection prevention, or cryptographic changes observed.
Evidence from the diff
The diff extends the ShopinBit request/address models and UI to include an optional deliveryState/state field, requiring it for U.S. and Canadian addresses except travel requests. It introduces a ShopInBitStatePicker widget, wires it into car research, concierge, shipping, and car-fee flows, and updates route argument records to pass a ShopInBitTicket object instead of separate apiTicketId/deliveryCountry values. A debug-only HTTP response log was also added in the ShopInBit client. No security fixes, input sanitization, authentication, cryptography, or authorization changes are present.
Changed components
lib/models/shopinbit/shopinbit_request_draft.dartlib/pages/shopinbit/shopinbit_car_fee_view.dartlib/pages/shopinbit/shopinbit_offer_view.dartlib/pages/shopinbit/shopinbit_shipping_view.dartlib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dartlib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dartlib/pages/shopinbit/step_4_components/shopinbit_country_picker.dartlib/pages/shopinbit/step_4_components/shopinbit_state_picker.dartlib/pages/shopinbit/step_4_components/shopinbit_travel_form.dartlib/route_generator.dartlib/services/shopinbit/src/client.dartlib/services/shopinbit/src/models/address.dartlib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dartInspect captured patch +441 / −189
diff --git a/lib/models/shopinbit/shopinbit_request_draft.dart b/lib/models/shopinbit/shopinbit_request_draft.dart
index 6265471..c4c3cdd 100644
--- a/lib/models/shopinbit/shopinbit_request_draft.dart
+++ b/lib/models/shopinbit/shopinbit_request_draft.dart
@@ -5,13 +5,20 @@ class ShopinbitRequestDraft {
final String requestDescription;
final String deliveryCountryName;
final String deliveryCountryCode;
+ final String? deliveryState;
final String? voucherCode;
+ bool get requiresState => switch (deliveryCountryCode) {
+ "US" || "CA" => category != .travel,
+ _ => false,
+ };
+
ShopinbitRequestDraft({
required this.category,
required this.requestDescription,
required this.deliveryCountryName,
required this.deliveryCountryCode,
+ required this.deliveryState,
required this.voucherCode,
});
@@ -20,6 +27,7 @@ class ShopinbitRequestDraft {
"requestDescription": requestDescription,
"deliveryCountryName": deliveryCountryName,
"deliveryCountryCode": deliveryCountryCode,
+ "deliveryState": deliveryState,
"voucherCode": voucherCode,
};
diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
index dcffe14..f656e41 100644
--- a/lib/pages/shopinbit/shopinbit_car_fee_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
@@ -134,6 +134,7 @@ class _ShopInBitCarFeeViewState extends ConsumerState<ShopInBitCarFeeView> {
zip: _billingPostalCodeController.text.trim(),
city: _billingCityController.text.trim(),
country: widget.draft.deliveryCountryCode,
+ state: widget.draft.requiresState ? widget.draft.deliveryState! : null,
);
// Cache the car request alongside billing so the backend failsafe can
@@ -361,12 +362,15 @@ class _ShopInBitCarFeeViewState extends ConsumerState<ShopInBitCarFeeView> {
spacing,
DetailItem(
- title: "Billing country",
+ title: "Country",
detail:
"${widget.draft.deliveryCountryName} "
"(${widget.draft.deliveryCountryCode})",
disableSelectableText: true,
),
+ if (widget.draft.requiresState) spacing,
+ if (widget.draft.requiresState)
+ DetailItem(title: "State", detail: widget.draft.deliveryState!),
if (!isDesktop) const Spacer(),
if (isDesktop) const SizedBox(height: 24),
PrimaryButton(
diff --git a/lib/pages/shopinbit/shopinbit_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart
index a8fd750..8b69335 100644
--- a/lib/pages/shopinbit/shopinbit_offer_view.dart
+++ b/lib/pages/shopinbit/shopinbit_offer_view.dart
@@ -196,11 +196,7 @@ class _ShopInBitOfferViewState extends ConsumerState<ShopInBitOfferView> {
if (context.mounted) {
await Navigator.of(context).pushNamed(
ShopInBitShippingView.routeName,
- arguments: (
- apiTicketId: widget.apiTicketId,
- deliveryCountry: deliveryCountry,
- countries: response!.value!,
- ),
+ arguments: (ticket: ticket!, countries: response!.value!),
);
}
},
diff --git a/lib/pages/shopinbit/shopinbit_shipping_view.dart b/lib/pages/shopinbit/shopinbit_shipping_view.dart
index c7419ef..e025bad 100644
--- a/lib/pages/shopinbit/shopinbit_shipping_view.dart
+++ b/lib/pages/shopinbit/shopinbit_shipping_view.dart
@@ -1,17 +1,13 @@
import 'dart:async';
-import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_svg/svg.dart';
-import '../../providers/db/drift_provider.dart';
+import '../../db/drift/shared_db/shared_database.dart';
import '../../providers/global/shopin_bit_service_provider.dart';
import '../../services/shopinbit/src/models/address.dart';
import '../../services/shopinbit/src/models/payment.dart';
import '../../themes/stack_colors.dart';
-import '../../utilities/assets.dart';
-import '../../utilities/constants.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
@@ -25,19 +21,19 @@ import '../../widgets/stack_dialog.dart';
import '../../widgets/textfields/adaptive_text_field.dart';
import 'shopinbit_payment_shared.dart';
import 'shopinbit_payment_view.dart';
+import 'step_4_components/shopinbit_country_picker.dart';
+import 'step_4_components/shopinbit_state_picker.dart';
class ShopInBitShippingView extends ConsumerStatefulWidget {
const ShopInBitShippingView({
super.key,
- required this.apiTicketId,
- required this.deliveryCountry,
+ required this.ticket,
required this.countries,
});
static const String routeName = "/shopInBitShipping";
- final int apiTicketId;
- final String deliveryCountry;
+ final ShopInBitTicket ticket;
final List<Map<String, dynamic>> countries;
@override
@@ -50,8 +46,6 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
late final TextEditingController _streetController;
late final TextEditingController _cityController;
late final TextEditingController _postalCodeController;
- final TextEditingController _countrySearchController =
- TextEditingController();
late final FocusNode _nameFocusNode;
late final FocusNode _streetFocusNode;
late final FocusNode _cityFocusNode;
@@ -62,8 +56,6 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
late final TextEditingController _billingStreetController;
late final TextEditingController _billingCityController;
late final TextEditingController _billingPostalCodeController;
- final TextEditingController _billingCountrySearchController =
- TextEditingController();
late final FocusNode _billingNameFocusNode;
late final FocusNode _billingStreetFocusNode;
late final FocusNode _billingCityFocusNode;
@@ -75,6 +67,12 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
late final String _selectedCountryIso;
late final String _deliveryCountryLabel;
+ late final String? _selectedState;
+
+ String? _selectedBillingState;
+
+ late bool _requiresState;
+
bool _submitting = false;
bool get _canContinue {
@@ -116,7 +114,32 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_billingCityFocusNode = FocusNode();
_billingPostalCodeFocusNode = FocusNode();
- _selectedCountryIso = widget.deliveryCountry;
+ _selectedCountryIso = widget.ticket.deliveryCountry;
+
+ _requiresState = switch (_selectedCountryIso) {
+ "US" || "CA" => widget.ticket.category != .travel,
+ _ => false,
+ };
+
+ if (_requiresState) {
+ final parts = widget.ticket.messages.firstOrNull?.content.split("\n");
+ if (parts == null) {
+ Logging.instance.f("Missing state/province where required!");
+ throw ArgumentError("Missing first ticket message");
+ }
+
+ final line = parts
+ .where((e) => e.startsWith("Delivery state:"))
+ .firstOrNull;
+ if (line == null) {
+ Logging.instance.f("Missing delivery state/province in first message!");
+ throw ArgumentError("Missing state/province in first ticket message");
+ }
+
+ _selectedState = line.replaceFirst("Delivery state:", "").trim();
+ } else {
+ _selectedState = null;
+ }
// firstWhere should never fail here as the caller of this widget must
// check that countries contains the expected value. Failure here should be
@@ -147,7 +170,6 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_streetController.dispose();
_cityController.dispose();
_postalCodeController.dispose();
- _countrySearchController.dispose();
_nameFocusNode.dispose();
_streetFocusNode.dispose();
_cityFocusNode.dispose();
@@ -156,7 +178,6 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_billingStreetController.dispose();
_billingCityController.dispose();
_billingPostalCodeController.dispose();
- _billingCountrySearchController.dispose();
_billingNameFocusNode.dispose();
_billingStreetFocusNode.dispose();
_billingCityFocusNode.dispose();
@@ -193,20 +214,16 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
street: _billingStreetController.text.trim(),
zip: _billingPostalCodeController.text.trim(),
city: _billingCityController.text.trim(),
- country: _billingSelectedCountryIso!,
+ country: _requiresState ? country : _billingSelectedCountryIso!,
+ state: _requiresState ? _selectedState : _selectedBillingState,
);
}
- final thisTicket = await ref
- .read(pSharedDrift)
- .shopInBitTicketsDao
- .getByApiId(widget.apiTicketId);
-
final resp = await ref
.read(pShopinBitService)
.client
.submitAddress(
- widget.apiTicketId,
+ widget.ticket.apiTicketId,
shipping: Address(
firstName: firstName,
lastName: lastName,
@@ -214,9 +231,10 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
zip: postalCode,
city: city,
country: country,
+ state: _requiresState ? _selectedState! : null,
),
billing: billingAddress,
- customerKey: thisTicket!.customerKey,
+ customerKey: widget.ticket.customerKey,
);
if (resp.hasError) {
@@ -226,8 +244,8 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
paymentInfo = await fetchShopInBitPaymentInfo(
ref.read(pShopinBitService).client,
- widget.apiTicketId,
- thisTicket.customerKey,
+ widget.ticket.apiTicketId,
+ widget.ticket.customerKey,
);
} catch (e, s) {
Logging.instance.e("submitAddress threw", error: e, stackTrace: s);
@@ -252,7 +270,10 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
await Navigator.of(context).pushNamed(
ShopInBitPaymentView.routeName,
- arguments: (apiTicketId: widget.apiTicketId, paymentInfo: paymentInfo),
+ arguments: (
+ apiTicketId: widget.ticket.apiTicketId,
+ paymentInfo: paymentInfo,
+ ),
);
}
@@ -336,11 +357,9 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
],
),
spacing,
- DetailItem(
- title: "Country",
- detail: _deliveryCountryLabel,
- disableSelectableText: true,
- ),
+ DetailItem(title: "State", detail: _selectedState!),
+ spacing,
+ DetailItem(title: "Country", detail: _deliveryCountryLabel),
spacing,
// Billing address toggle.
GestureDetector(
@@ -354,6 +373,7 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_billingCityController.clear();
_billingPostalCodeController.clear();
_billingSelectedCountryIso = null;
+ _selectedBillingState = null;
}
});
},
@@ -373,6 +393,7 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
_billingCityController.clear();
_billingPostalCodeController.clear();
_billingSelectedCountryIso = null;
+ _selectedBillingState = null;
}
});
},
@@ -447,123 +468,35 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
],
),
spacing,
- // Billing country dropdown.
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: DropdownButtonHideUnderline(
- child: DropdownButton2<String>(
- value: _billingSelectedCountryIso,
- items: widget.countries
- .map(
- (c) => DropdownMenuItem<String>(
- value: c['iso'] as String,
- child: Text(
- c['label'] as String,
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(
- context,
- ).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldActiveText,
- )
- : STextStyles.w500_14(context),
- ),
- ),
- )
- .toList(),
- onMenuStateChange: (isOpen) {
- if (!isOpen) {
- _billingCountrySearchController.clear();
- }
- },
- onChanged: (value) {
+
+ if (_requiresState) ...[
+ DetailItem(title: "Billing state", detail: _selectedState),
+ spacing,
+ DetailItem(title: "Billing country", detail: _deliveryCountryLabel),
+ ],
+
+ if (!_requiresState) ...[
+ ShopInBitStatePicker(
+ countryIso: _billingSelectedCountryIso!,
+ selectedState: _selectedBillingState,
+ onChanged: (state) {
+ if (state != _selectedBillingState && mounted) {
setState(() {
- _billingSelectedCountryIso = value;
+ _selectedBillingState = state;
});
- },
- hint: Text(
- "Country",
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldDefaultSearchIconLeft,
- )
- : STextStyles.fieldLabel(context),
- ),
- isExpanded: true,
- buttonStyleData: ButtonStyleData(
- decoration: BoxDecoration(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- ),
- ),
- iconStyleData: IconStyleData(
- icon: Padding(
- padding: const EdgeInsets.only(right: 10),
- child: SvgPicture.asset(
- Assets.svg.chevronDown,
- width: 12,
- height: 6,
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldActiveSearchIconRight,
- ),
- ),
- ),
- dropdownStyleData: DropdownStyleData(
- offset: const Offset(0, 0),
- elevation: 0,
- maxHeight: 300,
- decoration: BoxDecoration(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- ),
- ),
- dropdownSearchData: DropdownSearchData<String>(
- searchController: _billingCountrySearchController,
- searchInnerWidgetHeight: 48,
- searchInnerWidget: TextFormField(
- controller: _billingCountrySearchController,
- decoration: InputDecoration(
- isDense: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 14,
- ),
- hintText: "Search...",
- hintStyle: STextStyles.fieldLabel(context),
- border: InputBorder.none,
- ),
- ),
- searchMatchFn: (item, searchValue) {
- final label = widget.countries
- .where((c) => c['iso'] == item.value)
- .map((c) => c['label'] as String)
- .firstOrNull;
- return label?.toLowerCase().contains(
- searchValue.toLowerCase(),
- ) ??
- false;
- },
- ),
- menuItemStyleData: const MenuItemStyleData(
- padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- ),
- ),
+ }
+ },
),
- ),
+ spacing,
+ ShopInBitCountryPicker(
+ hintText: "Billing country",
+ selectedIso: _billingSelectedCountryIso,
+ onChanged: (data) => setState(() {
+ _billingSelectedCountryIso = data?.code;
+ _requiresState = data?.requiresState ?? false;
+ }),
+ ),
+ ],
],
const SizedBox(height: 24),
PrimaryButton(
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart
index 8fda016..d8e3fcd 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart
@@ -10,12 +10,14 @@ import "../../../themes/stack_colors.dart";
import "../../../utilities/assets.dart";
import "../../../utilities/text_styles.dart";
import "../../../utilities/util.dart";
+import "../../../widgets/conditional_parent.dart";
import "../../../widgets/rounded_white_container.dart";
import "../../../widgets/textfields/adaptive_text_field.dart";
import "../shopinbit_car_fee_view.dart";
import "shopinbit_country_picker.dart";
import "shopinbit_labeled_checkbox.dart";
import "shopinbit_privacy_checkbox.dart";
+import "shopinbit_state_picker.dart";
import "shopinbit_step4_dropdown.dart";
import "shopinbit_step4_header.dart";
import "shopinbit_step4_submit_button.dart";
@@ -56,6 +58,8 @@ class _ShopInBitCarResearchFormState
bool _feeAcknowledged = false;
String? _selectedCountryIsoCode;
String? _selectedCountryName;
+ String? _selectedState;
+ bool? _requiresState;
bool _privacyAccepted = false;
bool _submitting = false;
@@ -111,17 +115,23 @@ class _ShopInBitCarResearchFormState
try {
final countryIso = _selectedCountryIsoCode!;
+ final sb = StringBuffer();
+ sb.writeln("Brand: ${_brandController.text.trim()}");
+ sb.writeln("Model: ${_modelController.text.trim()}");
+ sb.writeln("Condition: $_selectedCarCondition");
+ sb.writeln("Description: ${_carDescriptionController.text.trim()}");
+ sb.writeln("Budget: ${_carBudgetController.text.trim()} EUR");
+ if (_requiresState == true) {
+ sb.writeln("Delivery state: ${_selectedState!}");
+ }
+ sb.writeln("Delivery country: $countryIso");
+
final draft = ShopinbitRequestDraft(
category: .car,
- requestDescription:
- "Brand: ${_brandController.text.trim()}\n"
- "Model: ${_modelController.text.trim()}\n"
- "Condition: $_selectedCarCondition\n"
- "Description: ${_carDescriptionController.text.trim()}\n"
- "Budget: ${_carBudgetController.text.trim()} EUR\n"
- "Delivery country: $countryIso",
+ requestDescription: sb.toString(),
deliveryCountryCode: countryIso,
deliveryCountryName: _selectedCountryName!,
+ deliveryState: _requiresState == true ? _selectedState! : null,
voucherCode: null,
);
@@ -180,12 +190,34 @@ class _ShopInBitCarResearchFormState
subtitle: "Tell us about the car you're looking for.",
),
SizedBox(height: isDesktop ? 32 : 24),
- ShopInBitCountryPicker(
- selectedIso: _selectedCountryIsoCode,
- onChanged: (data) => setState(() {
- _selectedCountryIsoCode = data?.code;
- _selectedCountryName = data?.name;
- }),
+ ConditionalParent(
+ condition: _requiresState == true,
+ builder: (child) => Column(
+ mainAxisSize: .min,
+ children: [
+ child,
+ SizedBox(height: isDesktop ? 24 : 16),
+ ShopInBitStatePicker(
+ countryIso: _selectedCountryIsoCode!,
+ selectedState: _selectedState,
+ onChanged: (state) {
+ if (state != _selectedState && mounted) {
+ setState(() {
+ _selectedState = state;
+ });
+ }
+ },
+ ),
+ ],
+ ),
+ child: ShopInBitCountryPicker(
+ selectedIso: _selectedCountryIsoCode,
+ onChanged: (data) => setState(() {
+ _selectedCountryIsoCode = data?.code;
+ _selectedCountryName = data?.name;
+ _requiresState = data?.requiresState;
+ }),
+ ),
),
SizedBox(height: isDesktop ? 24 : 16),
AdaptiveTextField(
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
index 781575c..8e39af3 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
@@ -5,10 +5,12 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "../../../models/shopinbit/shopinbit_request_draft.dart";
import "../../../providers/global/shopin_bit_service_provider.dart";
import "../../../utilities/util.dart";
+import "../../../widgets/conditional_parent.dart";
import "../../../widgets/textfields/adaptive_text_field.dart";
import "shopinbit_country_picker.dart";
import "shopinbit_labeled_checkbox.dart";
import "shopinbit_privacy_checkbox.dart";
+import "shopinbit_state_picker.dart";
import "shopinbit_step4_dropdown.dart";
import "shopinbit_step4_header.dart";
import "shopinbit_step4_submit.dart";
@@ -44,6 +46,8 @@ class _ShopInBitConciergeFormState
bool _noLimit = false;
String? _selectedCountryIsoCode;
String? _selectedCountryName;
+ String? _selectedState;
+ bool? _requiresState;
bool _privacyAccepted = false;
bool _submitting = false;
@@ -94,15 +98,19 @@ class _ShopInBitConciergeFormState
? "No limit"
: "${_budgetController.text.trim()} EUR";
+ final sb = StringBuffer();
+ sb.writeln("What to purchase: ${_whatToPurchaseController.text.trim()}");
+ sb.writeln("Condition: $_selectedCondition");
+ sb.writeln("Budget: $budgetText");
+ if (_requiresState == true) sb.writeln("State: ${_selectedState!}");
+ sb.writeln("Delivery country: $countryIso");
+
final draft = ShopinbitRequestDraft(
category: .concierge,
- requestDescription:
- "What to purchase: ${_whatToPurchaseController.text.trim()}\n"
- "Condition: $_selectedCondition\n"
- "Budget: $budgetText\n"
- "Delivery country: $countryIso",
+ requestDescription: sb.toString(),
deliveryCountryCode: countryIso,
deliveryCountryName: _selectedCountryName!,
+ deliveryState: _requiresState == true ? _selectedState! : null,
voucherCode: null,
);
@@ -177,12 +185,34 @@ class _ShopInBitConciergeFormState
label: "No budget limit",
),
SizedBox(height: isDesktop ? 24 : 20),
- ShopInBitCountryPicker(
- selectedIso: _selectedCountryIsoCode,
- onChanged: (data) => setState(() {
- _selectedCountryIsoCode = data?.code;
- _selectedCountryName = data?.name;
- }),
+ ConditionalParent(
+ condition: _requiresState == true,
+ builder: (child) => Column(
+ mainAxisSize: .min,
+ children: [
+ child,
+ SizedBox(height: isDesktop ? 24 : 16),
+ ShopInBitStatePicker(
+ countryIso: _selectedCountryIsoCode!,
+ selectedState: _selectedState,
+ onChanged: (state) {
+ if (state != _selectedState && mounted) {
+ setState(() {
+ _selectedState = state;
+ });
+ }
+ },
+ ),
+ ],
+ ),
+ child: ShopInBitCountryPicker(
+ selectedIso: _selectedCountryIsoCode,
+ onChanged: (data) => setState(() {
+ _selectedCountryIsoCode = data?.code;
+ _selectedCountryName = data?.name;
+ _requiresState = data?.requiresState;
+ }),
+ ),
),
SizedBox(height: isDesktop ? 16 : 24),
ShopInBitPrivacyCheckbox(
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
index b1ca9a6..5afd7e0 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
@@ -16,12 +16,16 @@ class ShopInBitCountryPicker extends ConsumerStatefulWidget {
required this.selectedIso,
required this.onChanged,
this.hintText = "Delivery country",
+ this.preLoadedCountries,
});
final String? selectedIso;
- final ValueChanged<({String name, String code})?> onChanged;
+ final ValueChanged<({String name, String code, bool requiresState})?>
+ onChanged;
final String hintText;
+ final List<Map<String, dynamic>>? preLoadedCountries;
+
@override
ConsumerState<ShopInBitCountryPicker> createState() =>
_ShopInBitCountryPickerState();
@@ -36,7 +40,11 @@ class _ShopInBitCountryPickerState
@override
void initState() {
super.initState();
- _fetchCountries();
+ if (widget.preLoadedCountries != null) {
+ _countries = widget.preLoadedCountries!;
+ } else {
+ _fetchCountries();
+ }
}
@override
@@ -104,6 +112,7 @@ class _ShopInBitCountryPickerState
_countries.firstWhere((e) => e["iso"] == iso)["label"]
as String,
code: iso!,
+ requiresState: iso == "CA" || iso == "US",
));
},
hint: Text(
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_state_picker.dart b/lib/pages/shopinbit/step_4_components/shopinbit_state_picker.dart
new file mode 100644
index 0000000..84ace39
--- /dev/null
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_state_picker.dart
@@ -0,0 +1,234 @@
+import "package:dropdown_button2/dropdown_button2.dart";
+import "package:flutter/material.dart";
+import "package:flutter_svg/svg.dart";
+
+import "../../../themes/stack_colors.dart";
+import "../../../utilities/assets.dart";
+import "../../../utilities/constants.dart";
+import "../../../utilities/text_styles.dart";
+import "../../../utilities/util.dart";
+
+const List<String> _usStates = [
+ "Alabama (AL)",
+ "Alaska (AK)",
+ "Arizona (AZ)",
+ "Arkansas (AR)",
+ "California (CA)",
+ "Colorado (CO)",
+ "Connecticut (CT)",
+ "Delaware (DE)",
+ "Florida (FL)",
+ "Georgia (GA)",
+ "Hawaii (HI)",
+ "Idaho (ID)",
+ "Illinois (IL)",
+ "Indiana (IN)",
+ "Iowa (IA)",
+ "Kansas (KS)",
+ "Kentucky (KY)",
+ "Louisiana (LA)",
+ "Maine (ME)",
+ "Maryland (MD)",
+ "Massachusetts (MA)",
+ "Michigan (MI)",
+ "Minnesota (MN)",
+ "Mississippi (MS)",
+ "Missouri (MO)",
+ "Montana (MT)",
+ "Nebraska (NE)",
+ "Nevada (NV)",
+ "New Hampshire (NH)",
+ "New Jersey (NJ)",
+ "New Mexico (NM)",
+ "New York (NY)",
+ "North Carolina (NC)",
+ "North Dakota (ND)",
+ "Ohio (OH)",
+ "Oklahoma (OK)",
+ "Oregon (OR)",
+ "Pennsylvania (PA)",
+ "Rhode Island (RI)",
+ "South Carolina (SC)",
+ "South Dakota (SD)",
+ "Tennessee (TN)",
+ "Texas (TX)",
+ "Utah (UT)",
+ "Vermont (VT)",
+ "Virginia (VA)",
+ "Washington (WA)",
+ "West Virginia (WV)",
+ "Wisconsin (WI)",
+
+ // Wyoming is now allowed as per chat with shopinbit
+ // "Wyoming (WY)",
+];
+
+const List<String> _canadaProvinces = [
+ "Alberta (AB)",
+ "British Columbia (BC)",
+ "Manitoba (MB)",
+ "New Brunswick (NB)",
+ "Newfoundland and Labrador (NL)",
+ "Northwest Territories (NT)",
+ "Nova Scotia (NS)",
+ "Nunavut (NU)",
+ "Ontario (ON)",
+ "Prince Edward Island (PE)",
+ "Quebec (QC)",
+ "Saskatchewan (SK)",
+ "Yukon (YT)",
+];
+
+List<String> _statesForCountry(String countryIso) => switch (countryIso) {
+ "US" => _usStates,
+ "CA" => _canadaProvinces,
+ _ => throw ArgumentError.value(countryIso, "countryIso", "Must be US or CA"),
+};
+
+String _hintTextForCountry(String countryIso) => switch (countryIso) {
+ "US" => "Select state",
+ "CA" => "Select province / territory",
+ _ => throw ArgumentError.value(countryIso, "countryIso", "Must be US or CA"),
+};
+
+class ShopInBitStatePicker extends StatefulWidget {
+ const ShopInBitStatePicker({
+ super.key,
+ required this.countryIso,
+ required this.selectedState,
+ required this.onChanged,
+ });
+
+ final String countryIso;
+ final String? selectedState;
+ final ValueChanged<String?> onChanged;
+
+ @override
+ State<ShopInBitStatePicker> createState() => _ShopInBitStatePickerState();
+}
+
+class _ShopInBitStatePickerState extends State<ShopInBitStatePicker> {
+ final TextEditingController _searchController = TextEditingController();
+
+ List<String> get _states => _statesForCountry(widget.countryIso);
+
+ String? get _validatedSelection {
+ final String? selected = widget.selectedState;
+ if (selected == null) return null;
+ return _states.contains(selected) ? selected : null;
+ }
+
+ @override
+ void didUpdateWidget(ShopInBitStatePicker oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.countryIso != widget.countryIso) {
+ // Invalidate selection when country changes.
+ if (widget.selectedState != null &&
+ !_statesForCountry(
+ widget.countryIso,
+ ).contains(widget.selectedState)) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ widget.onChanged(null);
+ });
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final StackColors stackColors = Theme.of(context).extension<StackColors>()!;
+
+ final TextStyle itemStyle = Util.isDesktop
+ ? STextStyles.desktopTextExtraSmall(
+ context,
+ ).copyWith(color: stackColors.textFieldActiveText)
+ : STextStyles.w500_14(context);
+
+ final TextStyle hintStyle = Util.isDesktop
+ ? STextStyles.desktopTextExtraSmall(
+ context,
+ ).copyWith(color: stackColors.textFieldDefaultSearchIconLeft)
+ : STextStyles.fieldLabel(context);
+
+ return DropdownButtonHideUnderline(
+ child: DropdownButton2<String>(
+ value: _validatedSelection,
+ items: _states
+ .map(
+ (state) => DropdownMenuItem<String>(
+ value: state,
+ child: Text(state, style: itemStyle),
+ ),
+ )
+ .toList(),
+ onMenuStateChange: (isOpen) {
+ if (!isOpen) {
+ _searchController.clear();
+ }
+ },
+ onChanged: widget.onChanged,
+ hint: Text(_hintTextForCountry(widget.countryIso), style: hintStyle),
+ isExpanded: true,
+ buttonStyleData: ButtonStyleData(
+ decoration: BoxDecoration(
+ color: stackColors.textFieldDefaultBG,
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ ),
+ iconStyleData: IconStyleData(
+ icon: Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: SvgPicture.asset(
+ Assets.svg.chevronDown,
+ width: 12,
+ height: 6,
+ color: stackColors.textFieldActiveSearchIconRight,
+ ),
+ ),
+ ),
+ dropdownStyleData: DropdownStyleData(
+ offset: const Offset(0, -10),
+ elevation: 0,
+ maxHeight: 300,
+ decoration: BoxDecoration(
+ color: stackColors.textFieldDefaultBG,
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ ),
+ dropdownSearchData: DropdownSearchData<String>(
+ searchController: _searchController,
+ searchInnerWidgetHeight: 48,
+ searchInnerWidget: TextFormField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ isDense: true,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 14,
+ ),
+ hintText: "Search...",
+ hintStyle: STextStyles.fieldLabel(context),
+ border: InputBorder.none,
+ ),
+ ),
+ searchMatchFn: (item, searchValue) =>
+ item.value?.toLowerCase().contains(searchValue.toLowerCase()) ??
+ false,
+ ),
+ menuItemStyleData: const MenuItemStyleData(
+ padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
index e246eb8..b557138 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
@@ -95,7 +95,6 @@ class _ShopInBitTravelFormState extends ConsumerState<ShopInBitTravelForm> {
String? _selectedArrangement;
String? _selectedDepartureCountryIso;
- String? _selectedDepartureCountryName;
String? _selectedDateMode;
String? _selectedFlexibility;
String? _selectedYear;
@@ -241,6 +240,7 @@ class _ShopInBitTravelFormState extends ConsumerState<ShopInBitTravelForm> {
deliveryCountryCode: "DE",
voucherCode: null,
deliveryCountryName: "Germany",
+ deliveryState: null,
);
try {
await submitShopInBitRequest(context, draft, ref.read(pShopinBitService));
@@ -328,7 +328,6 @@ class _ShopInBitTravelFormState extends ConsumerState<ShopInBitTravelForm> {
selectedIso: _selectedDepartureCountryIso,
onChanged: (data) => setState(() {
_selectedDepartureCountryIso = data?.code;
- _selectedDepartureCountryName = data?.name;
}),
hintText: "Departure country",
),
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index cbe2a11..049a808 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -15,6 +15,7 @@ import 'package:tuple/tuple.dart';
import 'app_config.dart';
import 'db/drift/database.dart';
+import 'db/drift/shared_db/shared_database.dart';
import 'models/add_wallet_list_entity/add_wallet_list_entity.dart';
import 'models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
import 'models/add_wallet_list_entity/sub_classes/sol_token_entity.dart';
@@ -1215,15 +1216,13 @@ class RouteGenerator {
case ShopInBitShippingView.routeName:
if (args
is ({
- int apiTicketId,
- String deliveryCountry,
+ ShopInBitTicket ticket,
List<Map<String, dynamic>> countries,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ShopInBitShippingView(
- apiTicketId: args.apiTicketId,
- deliveryCountry: args.deliveryCountry,
+ ticket: args.ticket,
countries: args.countries,
),
settings: RouteSettings(name: settings.name),
diff --git a/lib/services/shopinbit/src/client.dart b/lib/services/shopinbit/src/client.dart
index f5c46c3..0a1dd0b 100644
--- a/lib/services/shopinbit/src/client.dart
+++ b/lib/services/shopinbit/src/client.dart
@@ -2,6 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
+import 'package:flutter/foundation.dart';
+
import '../../../app_config.dart';
import '../../../networking/http.dart';
import '../../../utilities/logger.dart';
@@ -759,6 +761,13 @@ class ShopInBitClient {
customerKey: customerKey,
);
+ if (kDebugMode) {
+ Logging.instance.i(
+ "$_kTag $method HTTP:${response.code} "
+ "body: ${response.body}",
+ );
+ }
+
final resolved = _resolvePath(path);
if (response.code >= 200 && response.code < 300) {
diff --git a/lib/services/shopinbit/src/models/address.dart b/lib/services/shopinbit/src/models/address.dart
index 5371a37..63bce5b 100644
--- a/lib/services/shopinbit/src/models/address.dart
+++ b/lib/services/shopinbit/src/models/address.dart
@@ -18,7 +18,7 @@ class Address {
required this.zip,
required this.city,
required this.country,
- this.state,
+ required this.state,
});
Map<String, dynamic> toJson() => {
diff --git a/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart
index eed491d..9c0cbc2 100644
--- a/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart
+++ b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
+import '../../../db/drift/shared_db/shared_database.dart';
import '../../../models/shopinbit/shopinbit_enums.dart';
import '../../../models/shopinbit/shopinbit_request_draft.dart';
import '../../../pages/cakepay/cakepay_card_detail_view.dart';
@@ -156,14 +157,12 @@ abstract final class NestedNavigatorDialogRouteGenerator {
case ShopInBitShippingView.routeName:
if (args
is ({
- int apiTicketId,
- String deliveryCountry,
+ ShopInBitTicket ticket,
List<Map<String, dynamic>> countries,
})) {
return getRoute(
builder: (_) => ShopInBitShippingView(
- apiTicketId: args.apiTicketId,
- deliveryCountry: args.deliveryCountry,
+ ticket: args.ticket,
countries: args.countries,
),
settings: RouteSettings(name: settings.name),
Why this scored 19/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.