Skip to content

Commit 9f558ea

Browse files
committed
fix(shopinbit): keep delivery country consistent in shipping view
1 parent b4cb894 commit 9f558ea

1 file changed

Lines changed: 188 additions & 113 deletions

File tree

lib/pages/shopinbit/shopinbit_shipping_view.dart

Lines changed: 188 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
187187
postalCode: postalCode,
188188
country: country,
189189
);
190+
// Keep deliveryCountry authoritative and in sync with the shipping
191+
// country. No-op when it was already set (the normal flow); fills the gap
192+
// for restored orders, where deliveryCountry came back empty from the API
193+
// and the user picked one here.
194+
widget.model.deliveryCountry = country;
190195

191196
// Pre-load the payment info before pushing the payment view so it renders
192197
// populated immediately. The Continue button's spinner (_submitting)
@@ -260,6 +265,171 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
260265
);
261266
}
262267

268+
// Read-only display of the locked delivery country. Looks like the other
269+
// fields but isn't editable; the country was fixed when the offer was priced.
270+
Widget _buildLockedCountryField(
271+
BuildContext context, {
272+
required bool isDesktop,
273+
}) {
274+
final label =
275+
_countries
276+
.where((c) => c['iso'] == _selectedCountryIso)
277+
.map((c) => c['label'] as String)
278+
.firstOrNull ??
279+
(_selectedCountryIso ?? "");
280+
281+
return Container(
282+
decoration: BoxDecoration(
283+
color: Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
284+
borderRadius: BorderRadius.circular(
285+
Constants.size.circularBorderRadius,
286+
),
287+
),
288+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
289+
child: Row(
290+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
291+
children: [
292+
Text(
293+
"Country",
294+
style: isDesktop
295+
? STextStyles.desktopTextExtraSmall(context).copyWith(
296+
color: Theme.of(
297+
context,
298+
).extension<StackColors>()!.textFieldDefaultSearchIconLeft,
299+
)
300+
: STextStyles.fieldLabel(context),
301+
),
302+
Text(
303+
label,
304+
style: isDesktop
305+
? STextStyles.desktopTextExtraSmall(context).copyWith(
306+
color: Theme.of(
307+
context,
308+
).extension<StackColors>()!.textFieldActiveText,
309+
)
310+
: STextStyles.w500_14(context),
311+
),
312+
],
313+
),
314+
);
315+
}
316+
317+
// Editable, searchable country dropdown. Only shown when the delivery country
318+
// wasn't pre-set (restored-from-API orders).
319+
Widget _buildCountryDropdown(
320+
BuildContext context, {
321+
required bool isDesktop,
322+
}) {
323+
return ClipRRect(
324+
borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
325+
child: DropdownButtonHideUnderline(
326+
child: DropdownButton2<String>(
327+
value: _selectedCountryIso,
328+
items: _countries
329+
.map(
330+
(c) => DropdownMenuItem<String>(
331+
value: c['iso'] as String,
332+
child: Text(
333+
c['label'] as String,
334+
style: isDesktop
335+
? STextStyles.desktopTextExtraSmall(context).copyWith(
336+
color: Theme.of(
337+
context,
338+
).extension<StackColors>()!.textFieldActiveText,
339+
)
340+
: STextStyles.w500_14(context),
341+
),
342+
),
343+
)
344+
.toList(),
345+
onMenuStateChange: (isOpen) {
346+
if (!isOpen) {
347+
_countrySearchController.clear();
348+
}
349+
},
350+
onChanged: _loadingCountries
351+
? null
352+
: (value) => setState(() => _selectedCountryIso = value),
353+
hint: Text(
354+
_loadingCountries ? "Loading countries..." : "Country",
355+
style: isDesktop
356+
? STextStyles.desktopTextExtraSmall(context).copyWith(
357+
color: Theme.of(
358+
context,
359+
).extension<StackColors>()!.textFieldDefaultSearchIconLeft,
360+
)
361+
: STextStyles.fieldLabel(context),
362+
),
363+
isExpanded: true,
364+
buttonStyleData: ButtonStyleData(
365+
decoration: BoxDecoration(
366+
color: Theme.of(
367+
context,
368+
).extension<StackColors>()!.textFieldDefaultBG,
369+
borderRadius: BorderRadius.circular(
370+
Constants.size.circularBorderRadius,
371+
),
372+
),
373+
),
374+
iconStyleData: IconStyleData(
375+
icon: Padding(
376+
padding: const EdgeInsets.only(right: 10),
377+
child: SvgPicture.asset(
378+
Assets.svg.chevronDown,
379+
width: 12,
380+
height: 6,
381+
color: Theme.of(
382+
context,
383+
).extension<StackColors>()!.textFieldActiveSearchIconRight,
384+
),
385+
),
386+
),
387+
dropdownStyleData: DropdownStyleData(
388+
offset: const Offset(0, 0),
389+
elevation: 0,
390+
maxHeight: 300,
391+
decoration: BoxDecoration(
392+
color: Theme.of(
393+
context,
394+
).extension<StackColors>()!.textFieldDefaultBG,
395+
borderRadius: BorderRadius.circular(
396+
Constants.size.circularBorderRadius,
397+
),
398+
),
399+
),
400+
dropdownSearchData: DropdownSearchData<String>(
401+
searchController: _countrySearchController,
402+
searchInnerWidgetHeight: 48,
403+
searchInnerWidget: TextFormField(
404+
controller: _countrySearchController,
405+
decoration: InputDecoration(
406+
isDense: true,
407+
contentPadding: const EdgeInsets.symmetric(
408+
horizontal: 16,
409+
vertical: 14,
410+
),
411+
hintText: "Search...",
412+
hintStyle: STextStyles.fieldLabel(context),
413+
border: InputBorder.none,
414+
),
415+
),
416+
searchMatchFn: (item, searchValue) {
417+
final label = _countries
418+
.where((c) => c['iso'] == item.value)
419+
.map((c) => c['label'] as String)
420+
.firstOrNull;
421+
return label?.toLowerCase().contains(searchValue.toLowerCase()) ??
422+
false;
423+
},
424+
),
425+
menuItemStyleData: const MenuItemStyleData(
426+
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
427+
),
428+
),
429+
),
430+
);
431+
}
432+
263433
@override
264434
Widget build(BuildContext context) {
265435
final isDesktop = Util.isDesktop;
@@ -327,120 +497,25 @@ class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
327497
],
328498
),
329499
spacing,
330-
ClipRRect(
331-
borderRadius: BorderRadius.circular(
332-
Constants.size.circularBorderRadius,
333-
),
334-
child: DropdownButtonHideUnderline(
335-
child: DropdownButton2<String>(
336-
value: _selectedCountryIso,
337-
items: _countries
338-
.map(
339-
(c) => DropdownMenuItem<String>(
340-
value: c['iso'] as String,
341-
child: Text(
342-
c['label'] as String,
343-
style: isDesktop
344-
? STextStyles.desktopTextExtraSmall(
345-
context,
346-
).copyWith(
347-
color: Theme.of(
348-
context,
349-
).extension<StackColors>()!.textFieldActiveText,
350-
)
351-
: STextStyles.w500_14(context),
352-
),
353-
),
354-
)
355-
.toList(),
356-
onMenuStateChange: (isOpen) {
357-
if (!isOpen) {
358-
_countrySearchController.clear();
359-
}
360-
},
361-
onChanged: (_countryLocked || _loadingCountries)
362-
? null
363-
: (value) => setState(() => _selectedCountryIso = value),
364-
hint: Text(
365-
_loadingCountries ? "Loading countries..." : "Country",
366-
style: isDesktop
367-
? STextStyles.desktopTextExtraSmall(context).copyWith(
368-
color: Theme.of(context)
369-
.extension<StackColors>()!
370-
.textFieldDefaultSearchIconLeft,
371-
)
372-
: STextStyles.fieldLabel(context),
373-
),
374-
isExpanded: true,
375-
buttonStyleData: ButtonStyleData(
376-
decoration: BoxDecoration(
377-
color: Theme.of(
378-
context,
379-
).extension<StackColors>()!.textFieldDefaultBG,
380-
borderRadius: BorderRadius.circular(
381-
Constants.size.circularBorderRadius,
382-
),
383-
),
384-
),
385-
iconStyleData: IconStyleData(
386-
icon: Padding(
387-
padding: const EdgeInsets.only(right: 10),
388-
child: SvgPicture.asset(
389-
Assets.svg.chevronDown,
390-
width: 12,
391-
height: 6,
392-
color: Theme.of(
393-
context,
394-
).extension<StackColors>()!.textFieldActiveSearchIconRight,
395-
),
396-
),
397-
),
398-
dropdownStyleData: DropdownStyleData(
399-
offset: const Offset(0, 0),
400-
elevation: 0,
401-
maxHeight: 300,
402-
decoration: BoxDecoration(
403-
color: Theme.of(
404-
context,
405-
).extension<StackColors>()!.textFieldDefaultBG,
406-
borderRadius: BorderRadius.circular(
407-
Constants.size.circularBorderRadius,
408-
),
409-
),
410-
),
411-
dropdownSearchData: DropdownSearchData<String>(
412-
searchController: _countrySearchController,
413-
searchInnerWidgetHeight: 48,
414-
searchInnerWidget: TextFormField(
415-
controller: _countrySearchController,
416-
decoration: InputDecoration(
417-
isDense: true,
418-
contentPadding: const EdgeInsets.symmetric(
419-
horizontal: 16,
420-
vertical: 14,
421-
),
422-
hintText: "Search...",
423-
hintStyle: STextStyles.fieldLabel(context),
424-
border: InputBorder.none,
425-
),
426-
),
427-
searchMatchFn: (item, searchValue) {
428-
final label = _countries
429-
.where((c) => c['iso'] == item.value)
430-
.map((c) => c['label'] as String)
431-
.firstOrNull;
432-
return label?.toLowerCase().contains(
433-
searchValue.toLowerCase(),
434-
) ??
435-
false;
436-
},
437-
),
438-
menuItemStyleData: const MenuItemStyleData(
439-
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
440-
),
441-
),
500+
// The delivery country was chosen when the offer was requested and the
501+
// price (incl. shipping + VAT) was calculated from it, so it can't be
502+
// changed here. Restored-from-API orders are the exception: they come
503+
// back with no country, so we let the user supply one (and warn that it
504+
// may not match what the offer was priced for).
505+
if (_countryLocked)
506+
_buildLockedCountryField(context, isDesktop: isDesktop)
507+
else ...[
508+
_buildCountryDropdown(context, isDesktop: isDesktop),
509+
SizedBox(height: isDesktop ? 8 : 6),
510+
Text(
511+
"This order was started on another device. Choosing a country "
512+
"here may not match the delivery destination the offer was "
513+
"priced for.",
514+
style: isDesktop
515+
? STextStyles.desktopTextSmall(context)
516+
: STextStyles.itemSubtitle(context),
442517
),
443-
),
518+
],
444519
spacing,
445520
// Billing address toggle.
446521
GestureDetector(

0 commit comments

Comments
 (0)