feat(shopinbit): guidelines persistence, billing address, and setup view
What changed, and why it matters
This commit adds new user-facing features for the ShopinBit service inside the Stack Wallet app: a first-time setup screen that generates and displays a customer recovery key, a persistent display name, the ability to save that you've accepted service guidelines, and an optional separate billing address when entering shipping details. There is no indication in the commit that these changes fix a security bug or introduce a security vulnerability.
No security action required. As with any feature handling recovery keys and personal address data, ensure the customer key is stored securely, the clipboard copy action is intentional, and address data is transmitted over TLS and handled according to privacy requirements.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is a feature commit for the ShopinBit integration. It introduces ShopInBitSetupView for onboarding with ensureCustomerKey() and setDisplayName(), persists shopInBitGuidelinesAccepted, shopInBitSetupComplete, and shopInBitDisplayName in the app’s preferences box, and extends ShopInBitShippingView to collect and submit an optional billing address. It also adds a required checkbox for accepting guidelines on mobile. No cryptographic, network, or access-control changes with security implications are visible.
Changed components
lib/pages/more_view/services_view.dartlib/pages/shopinbit/shopinbit_setup_view.dartlib/pages/shopinbit/shopinbit_shipping_view.dartlib/pages/shopinbit/shopinbit_step_2.dartlib/pages/shopinbit/shopinbit_step_3.dartlib/route_generator.dartlib/services/shopinbit/shopinbit_service.dartInspect captured patch +649 / −18
diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart
index 2c33d61..ee33ceb 100644
--- a/lib/pages/more_view/services_view.dart
+++ b/lib/pages/more_view/services_view.dart
@@ -14,8 +14,11 @@ import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
+import '../../services/shopinbit/shopinbit_service.dart';
import '../shopinbit/shopinbit_settings_view.dart';
+import '../shopinbit/shopinbit_setup_view.dart';
import '../shopinbit/shopinbit_step_1.dart';
+import '../shopinbit/shopinbit_step_2.dart';
import '../shopinbit/shopinbit_tickets_view.dart';
class ServicesView extends StatefulWidget {
@@ -138,10 +141,27 @@ class _ServicesViewState extends State<ServicesView> {
.getPrimaryEnabledButtonStyle(dialogContext),
onPressed: () async {
Navigator.of(dialogContext).pop();
- await Navigator.of(context).pushNamed(
- ShopInBitStep1.routeName,
- arguments: ShopInBitOrderModel(),
- );
+ final model = ShopInBitOrderModel();
+ final service = ShopInBitService.instance;
+
+ if (service.loadSetupComplete()) {
+ // Returning user: pre-load display name,
+ // skip Step 1, go to Step 2
+ final savedName = service.loadDisplayName();
+ if (savedName != null && savedName.isNotEmpty) {
+ model.displayName = savedName;
+ }
+ await Navigator.of(context).pushNamed(
+ ShopInBitStep2.routeName,
+ arguments: model,
+ );
+ } else {
+ // First-time user: show setup flow
+ await Navigator.of(context).pushNamed(
+ ShopInBitSetupView.routeName,
+ arguments: model,
+ );
+ }
if (mounted) setState(() {});
},
child: Text(
diff --git a/lib/pages/shopinbit/shopinbit_setup_view.dart b/lib/pages/shopinbit/shopinbit_setup_view.dart
new file mode 100644
index 0000000..b917b68
--- /dev/null
+++ b/lib/pages/shopinbit/shopinbit_setup_view.dart
@@ -0,0 +1,205 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../notifications/show_flush_bar.dart';
+import '../../services/shopinbit/shopinbit_service.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/constants.dart';
+import '../../utilities/text_styles.dart';
+import '../../widgets/background.dart';
+import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/rounded_white_container.dart';
+import '../../widgets/stack_text_field.dart';
+import 'shopinbit_step_2.dart';
+
+class ShopInBitSetupView extends StatefulWidget {
+ const ShopInBitSetupView({super.key, required this.model});
+
+ static const String routeName = "/shopInBitSetup";
+
+ final ShopInBitOrderModel model;
+
+ @override
+ State<ShopInBitSetupView> createState() => _ShopInBitSetupViewState();
+}
+
+class _ShopInBitSetupViewState extends State<ShopInBitSetupView> {
+ late final Future<String> _keyFuture;
+ late final TextEditingController _nameController;
+ late final FocusNode _nameFocusNode;
+
+ bool get _canContinue => _nameController.text.trim().isNotEmpty;
+
+ @override
+ void initState() {
+ super.initState();
+ _keyFuture = ShopInBitService.instance.ensureCustomerKey();
+ _nameController = TextEditingController();
+ _nameFocusNode = FocusNode();
+
+ _nameFocusNode.addListener(() {
+ setState(() {});
+ });
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _nameFocusNode.dispose();
+ super.dispose();
+ }
+
+ Future<void> _completeSetup() async {
+ final name = _nameController.text.trim();
+ widget.model.displayName = name;
+ await ShopInBitService.instance.setDisplayName(name);
+ await ShopInBitService.instance.setSetupComplete(true);
+
+ if (mounted) {
+ Navigator.of(context).pushReplacementNamed(
+ ShopInBitStep2.routeName,
+ arguments: widget.model,
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ 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: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ "Your ShopinBit Customer Key",
+ style: STextStyles.pageTitleH1(context),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "This is your ShopinBit customer key. Save it "
+ "somewhere safe: you'll need it to recover "
+ "your ShopinBit account on a new device.",
+ style: STextStyles.itemSubtitle(context),
+ ),
+ const SizedBox(height: 16),
+ FutureBuilder<String>(
+ future: _keyFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState !=
+ ConnectionState.done) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+ if (snapshot.hasError) {
+ return Text(
+ "Failed to generate key. Please try again.",
+ style: STextStyles.itemSubtitle(
+ context,
+ ).copyWith(
+ color:
+ Theme.of(context)
+ .extension<StackColors>()!
+ .textError,
+ ),
+ );
+ }
+ final key = snapshot.data!;
+ return RoundedWhiteContainer(
+ child: Row(
+ children: [
+ Expanded(
+ child: SelectableText(
+ key,
+ style: STextStyles.itemSubtitle12(
+ context,
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.copy, size: 20),
+ onPressed: () {
+ Clipboard.setData(
+ ClipboardData(text: key),
+ );
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message: "Copied to clipboard!",
+ context: context,
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ const SizedBox(height: 32),
+ Text(
+ "Set a Display Name to use with ShopinBit staff",
+ style: STextStyles.smallMed12(context),
+ ),
+ const SizedBox(height: 8),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ controller: _nameController,
+ focusNode: _nameFocusNode,
+ autocorrect: false,
+ enableSuggestions: false,
+ onChanged: (_) => setState(() {}),
+ style: STextStyles.field(context),
+ decoration: standardInputDecoration(
+ "Display name",
+ _nameFocusNode,
+ context,
+ ).copyWith(
+ filled: true,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+ const Spacer(),
+ PrimaryButton(
+ label: "Complete Setup",
+ enabled: _canContinue,
+ onPressed: _canContinue ? _completeSetup : null,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/shopinbit/shopinbit_shipping_view.dart b/lib/pages/shopinbit/shopinbit_shipping_view.dart
index 1a499b7..8f24645 100644
--- a/lib/pages/shopinbit/shopinbit_shipping_view.dart
+++ b/lib/pages/shopinbit/shopinbit_shipping_view.dart
@@ -44,19 +44,45 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
late final FocusNode _cityFocusNode;
late final FocusNode _postalCodeFocusNode;
+ // Billing address controllers
+ late final TextEditingController _billingNameController;
+ 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;
+ late final FocusNode _billingPostalCodeFocusNode;
+
+ String? _billingSelectedCountryIso;
+ bool _differentBilling = false;
+
List<Map<String, dynamic>> _countries = [];
String? _selectedCountryIso;
bool _loadingCountries = false;
bool _submitting = false;
- bool get _canContinue =>
- !_submitting &&
- _nameController.text.trim().isNotEmpty &&
- _streetController.text.trim().isNotEmpty &&
- _cityController.text.trim().isNotEmpty &&
- _postalCodeController.text.trim().isNotEmpty &&
- _selectedCountryIso != null;
+ bool get _canContinue {
+ if (_submitting) return false;
+ final shippingValid =
+ _nameController.text.trim().isNotEmpty &&
+ _streetController.text.trim().isNotEmpty &&
+ _cityController.text.trim().isNotEmpty &&
+ _postalCodeController.text.trim().isNotEmpty &&
+ _selectedCountryIso != null;
+ if (!shippingValid) return false;
+ if (_differentBilling) {
+ return _billingNameController.text.trim().isNotEmpty &&
+ _billingStreetController.text.trim().isNotEmpty &&
+ _billingCityController.text.trim().isNotEmpty &&
+ _billingPostalCodeController.text.trim().isNotEmpty &&
+ _billingSelectedCountryIso != null;
+ }
+ return true;
+ }
@override
void initState() {
@@ -70,11 +96,24 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
_cityFocusNode = FocusNode();
_postalCodeFocusNode = FocusNode();
+ _billingNameController = TextEditingController();
+ _billingStreetController = TextEditingController();
+ _billingCityController = TextEditingController();
+ _billingPostalCodeController = TextEditingController();
+ _billingNameFocusNode = FocusNode();
+ _billingStreetFocusNode = FocusNode();
+ _billingCityFocusNode = FocusNode();
+ _billingPostalCodeFocusNode = FocusNode();
+
for (final node in [
_nameFocusNode,
_streetFocusNode,
_cityFocusNode,
_postalCodeFocusNode,
+ _billingNameFocusNode,
+ _billingStreetFocusNode,
+ _billingCityFocusNode,
+ _billingPostalCodeFocusNode,
]) {
node.addListener(() => setState(() {}));
}
@@ -93,6 +132,15 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
_streetFocusNode.dispose();
_cityFocusNode.dispose();
_postalCodeFocusNode.dispose();
+ _billingNameController.dispose();
+ _billingStreetController.dispose();
+ _billingCityController.dispose();
+ _billingPostalCodeController.dispose();
+ _billingCountrySearchController.dispose();
+ _billingNameFocusNode.dispose();
+ _billingStreetFocusNode.dispose();
+ _billingCityFocusNode.dispose();
+ _billingPostalCodeFocusNode.dispose();
super.dispose();
}
@@ -136,6 +184,24 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
final firstName = parts.first;
final lastName = parts.length > 1 ? parts.sublist(1).join(' ') : '';
+ Address? billingAddress;
+ if (_differentBilling) {
+ final billingName = _billingNameController.text.trim();
+ final billingParts = billingName.split(' ');
+ final billingFirst = billingParts.first;
+ final billingLast = billingParts.length > 1
+ ? billingParts.sublist(1).join(' ')
+ : '';
+ billingAddress = Address(
+ firstName: billingFirst,
+ lastName: billingLast,
+ street: _billingStreetController.text.trim(),
+ zip: _billingPostalCodeController.text.trim(),
+ city: _billingCityController.text.trim(),
+ country: _billingSelectedCountryIso!,
+ );
+ }
+
final resp = await ShopInBitService.instance.client.submitAddress(
widget.model.apiTicketId,
shipping: Address(
@@ -146,11 +212,11 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
city: city,
country: country,
),
+ billing: billingAddress,
);
if (resp.hasError) {
- // Address submission may fail in sandbox (pricing not calculated).
- // Log but proceed to payment.
+ // Sandbox may fail here; continue anyway.
debugPrint("submitAddress failed: ${resp.exception?.message}");
}
} catch (e) {
@@ -393,6 +459,223 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
),
),
),
+ spacing,
+ // Billing address toggle.
+ GestureDetector(
+ onTap: () {
+ setState(() {
+ _differentBilling = !_differentBilling;
+ if (!_differentBilling) {
+ // Clear billing fields.
+ _billingNameController.clear();
+ _billingStreetController.clear();
+ _billingCityController.clear();
+ _billingPostalCodeController.clear();
+ _billingSelectedCountryIso = null;
+ }
+ });
+ },
+ child: Row(
+ children: [
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: Checkbox(
+ value: _differentBilling,
+ onChanged: (v) {
+ setState(() {
+ _differentBilling = v ?? false;
+ if (!_differentBilling) {
+ _billingNameController.clear();
+ _billingStreetController.clear();
+ _billingCityController.clear();
+ _billingPostalCodeController.clear();
+ _billingSelectedCountryIso = null;
+ }
+ });
+ },
+ activeColor: Theme.of(context)
+ .extension<StackColors>()!
+ .accentColorBlue,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ "Different billing address?",
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.itemSubtitle(context),
+ ),
+ ),
+ ],
+ ),
+ ),
+ // Billing fields (expanded).
+ if (_differentBilling) ...[
+ SizedBox(height: isDesktop ? 24 : 16),
+ Text(
+ "Billing address",
+ style: isDesktop
+ ? STextStyles.desktopTextMedium(context)
+ : STextStyles.titleBold12(context),
+ ),
+ spacing,
+ _buildField(
+ controller: _billingNameController,
+ focusNode: _billingNameFocusNode,
+ label: "Full name",
+ isDesktop: isDesktop,
+ ),
+ spacing,
+ _buildField(
+ controller: _billingStreetController,
+ focusNode: _billingStreetFocusNode,
+ label: "Street address",
+ isDesktop: isDesktop,
+ ),
+ spacing,
+ Row(
+ children: [
+ Expanded(
+ child: _buildField(
+ controller: _billingCityController,
+ focusNode: _billingCityFocusNode,
+ label: "City",
+ isDesktop: isDesktop,
+ ),
+ ),
+ SizedBox(width: isDesktop ? 16 : 12),
+ Expanded(
+ child: _buildField(
+ controller: _billingPostalCodeController,
+ focusNode: _billingPostalCodeFocusNode,
+ label: "Postal code",
+ isDesktop: isDesktop,
+ ),
+ ),
+ ],
+ ),
+ spacing,
+ // Billing country dropdown.
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: DropdownButtonHideUnderline(
+ child: DropdownButton2<String>(
+ value: _billingSelectedCountryIso,
+ items: _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: _loadingCountries
+ ? null
+ : (value) {
+ setState(() {
+ _billingSelectedCountryIso = value;
+ });
+ },
+ hint: Text(
+ _loadingCountries ? "Loading countries..." : "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 = _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),
+ ),
+ ),
+ ),
+ ),
+ ],
const Spacer(),
PrimaryButton(
label: _submitting ? "Submitting..." : "Continue to payment",
diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart
index b8e4c7e..bed8a50 100644
--- a/lib/pages/shopinbit/shopinbit_step_2.dart
+++ b/lib/pages/shopinbit/shopinbit_step_2.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../services/shopinbit/shopinbit_service.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
@@ -13,6 +14,7 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_3.dart';
+import 'shopinbit_step_4.dart';
class ShopInBitStep2 extends StatefulWidget {
const ShopInBitStep2({super.key, required this.model});
@@ -46,16 +48,32 @@ class _ShopInBitStep2State extends State<ShopInBitStep2> {
void _continue() {
widget.model.category = _selected;
+
+ final skipGuidelines =
+ ShopInBitService.instance.loadGuidelinesAccepted();
+
if (Util.isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
showDialog<void>(
context: context,
- builder: (_) => ShopInBitStep3(model: widget.model),
+ builder: (_) => skipGuidelines
+ ? ShopInBitStep4(model: widget.model)
+ : ShopInBitStep3(model: widget.model),
);
} else {
- Navigator.of(
- context,
- ).pushNamed(ShopInBitStep3.routeName, arguments: widget.model);
+ if (skipGuidelines) {
+ // Returning user — skip guidelines.
+ widget.model.guidelinesAccepted = true;
+ Navigator.of(context).pushNamed(
+ ShopInBitStep4.routeName,
+ arguments: widget.model,
+ );
+ } else {
+ Navigator.of(context).pushNamed(
+ ShopInBitStep3.routeName,
+ arguments: widget.model,
+ );
+ }
}
}
diff --git a/lib/pages/shopinbit/shopinbit_step_3.dart b/lib/pages/shopinbit/shopinbit_step_3.dart
index bde00df..2f03093 100644
--- a/lib/pages/shopinbit/shopinbit_step_3.dart
+++ b/lib/pages/shopinbit/shopinbit_step_3.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../services/shopinbit/shopinbit_service.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
@@ -25,6 +26,8 @@ class ShopInBitStep3 extends StatefulWidget {
}
class _ShopInBitStep3State extends State<ShopInBitStep3> {
+ bool _agreed = false;
+
String _guidelinesText() {
switch (widget.model.category) {
case ShopInBitCategory.concierge:
@@ -72,6 +75,8 @@ class _ShopInBitStep3State extends State<ShopInBitStep3> {
void _continue() {
widget.model.guidelinesAccepted = true;
+ // Persist acceptance.
+ ShopInBitService.instance.setGuidelinesAccepted(true);
if (Util.isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
showDialog<void>(
@@ -125,8 +130,25 @@ class _ShopInBitStep3State extends State<ShopInBitStep3> {
),
),
),
+ if (!isDesktop)
+ CheckboxListTile(
+ value: _agreed,
+ onChanged: (v) => setState(() => _agreed = v ?? false),
+ title: Text(
+ "I have read and agree to the Service Guidelines",
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ controlAffinity: ListTileControlAffinity.leading,
+ contentPadding: EdgeInsets.zero,
+ activeColor:
+ Theme.of(context).extension<StackColors>()!.accentColorBlue,
+ ),
SizedBox(height: isDesktop ? 24 : 16),
- PrimaryButton(label: "Next", onPressed: _continue),
+ PrimaryButton(
+ label: "Next",
+ enabled: isDesktop || _agreed,
+ onPressed: (isDesktop || _agreed) ? _continue : null,
+ ),
],
);
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index 6fcb516..c829d19 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -180,6 +180,7 @@ import 'pages/shopinbit/shopinbit_order_created.dart';
import 'pages/shopinbit/shopinbit_payment_view.dart';
import 'pages/shopinbit/shopinbit_send_from_view.dart';
import 'pages/shopinbit/shopinbit_settings_view.dart';
+import 'pages/shopinbit/shopinbit_setup_view.dart';
import 'pages/shopinbit/shopinbit_shipping_view.dart';
import 'pages/shopinbit/shopinbit_step_1.dart';
import 'pages/shopinbit/shopinbit_step_2.dart';
@@ -1072,6 +1073,16 @@ class RouteGenerator {
// settings: RouteSettings(name: settings.name),
// );
+ case ShopInBitSetupView.routeName:
+ if (args is ShopInBitOrderModel) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => ShopInBitSetupView(model: args),
+ settings: RouteSettings(name: settings.name),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case ShopInBitStep1.routeName:
if (args is ShopInBitOrderModel) {
return getRoute(
diff --git a/lib/services/shopinbit/shopinbit_service.dart b/lib/services/shopinbit/shopinbit_service.dart
index 75ec3c0..5bc0639 100644
--- a/lib/services/shopinbit/shopinbit_service.dart
+++ b/lib/services/shopinbit/shopinbit_service.dart
@@ -9,6 +9,9 @@ class ShopInBitService {
ShopInBitClient? _client;
String? _customerKey;
+ bool? _guidelinesAccepted;
+ bool? _setupComplete;
+ String? _displayName;
ShopInBitClient get client {
return _client ??= ShopInBitClient(
@@ -81,4 +84,73 @@ class ShopInBitService {
);
Logging.instance.i("ShopInBitService: customer key cleared");
}
+
+ bool loadGuidelinesAccepted() {
+ if (_guidelinesAccepted != null) return _guidelinesAccepted!;
+ _guidelinesAccepted =
+ DB.instance.get<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitGuidelinesAccepted",
+ )
+ as bool? ??
+ false;
+ return _guidelinesAccepted!;
+ }
+
+ Future<void> setGuidelinesAccepted(bool accepted) async {
+ _guidelinesAccepted = accepted;
+ await DB.instance.put<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitGuidelinesAccepted",
+ value: accepted,
+ );
+ Logging.instance.i(
+ "ShopInBitService: guidelines accepted set to $accepted",
+ );
+ }
+
+ bool loadSetupComplete() {
+ if (_setupComplete != null) return _setupComplete!;
+ _setupComplete =
+ DB.instance.get<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitSetupComplete",
+ )
+ as bool? ??
+ false;
+ return _setupComplete!;
+ }
+
+ Future<void> setSetupComplete(bool complete) async {
+ _setupComplete = complete;
+ await DB.instance.put<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitSetupComplete",
+ value: complete,
+ );
+ Logging.instance.i(
+ "ShopInBitService: setup complete set to $complete",
+ );
+ }
+
+ String? loadDisplayName() {
+ if (_displayName != null) return _displayName;
+ _displayName =
+ DB.instance.get<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitDisplayName",
+ )
+ as String?;
+ return _displayName;
+ }
+
+ Future<void> setDisplayName(String name) async {
+ _displayName = name;
+ await DB.instance.put<dynamic>(
+ boxName: DB.boxNamePrefs,
+ key: "shopInBitDisplayName",
+ value: name,
+ );
+ Logging.instance.i("ShopInBitService: display name set");
+ }
}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.