From 7615bd77fea6a8d8bad0cc04f03e40d15bb88029 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:22:18 -0600 Subject: [PATCH 01/27] chore(ui): WIP clean up --- lib/pages/cakepay/cakepay_order_view.dart | 115 ++++---------- lib/pages/cakepay/cakepay_orders_view.dart | 17 +-- .../sub_widgets/exchange_provider_option.dart | 5 +- .../global_settings_view/hidden_settings.dart | 142 ------------------ lib/pages/shopinbit/shopinbit_offer_view.dart | 18 +-- .../shopinbit/shopinbit_payment_view.dart | 13 +- .../shopinbit/shopinbit_ticket_detail.dart | 10 +- .../shopinbit/shopinbit_tickets_view.dart | 10 +- lib/services/cakepay/cakepay_service.dart | 5 - 9 files changed, 50 insertions(+), 285 deletions(-) diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart index 71c9fe89a..aa11d295a 100644 --- a/lib/pages/cakepay/cakepay_order_view.dart +++ b/lib/pages/cakepay/cakepay_order_view.dart @@ -23,6 +23,7 @@ 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/loading_indicator.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; import 'cakepay_send_from_view.dart'; @@ -215,12 +216,7 @@ class _CakePayOrderViewState extends ConsumerState { setState(() { _loading = false; if (!resp.hasError && resp.value != null) { - var order = resp.value!; - final override = CakePayService.devStatusOverrides[order.orderId]; - if (override != null) { - order = order.copyWith(status: override); - } - _order = order; + _order = resp.value!; if (_isTerminal(_order!.status)) { _pollTimer?.cancel(); _countdownTimer?.cancel(); @@ -324,60 +320,6 @@ class _CakePayOrderViewState extends ConsumerState { ]; } - String _statusLabel(CakePayOrderStatus status) { - switch (status) { - case CakePayOrderStatus.new_: - return "New"; - case CakePayOrderStatus.expiredButStillPending: - return "Expired (pending)"; - case CakePayOrderStatus.expired: - return "Expired"; - case CakePayOrderStatus.failed: - return "Failed"; - case CakePayOrderStatus.paid: - return "Paid"; - case CakePayOrderStatus.paidPartial: - return "Partially paid"; - case CakePayOrderStatus.pendingPurchase: - return "Pending purchase"; - case CakePayOrderStatus.purchaseProcessing: - return "Processing"; - case CakePayOrderStatus.purchased: - return "Purchased"; - case CakePayOrderStatus.pendingEmail: - return "Pending email"; - case CakePayOrderStatus.complete: - return "Complete"; - case CakePayOrderStatus.pendingRefund: - return "Pending refund"; - case CakePayOrderStatus.refunded: - return "Refunded"; - } - } - - Color _statusColor(BuildContext context, CakePayOrderStatus status) { - final colors = Theme.of(context).extension()!; - switch (status) { - case CakePayOrderStatus.complete: - case CakePayOrderStatus.purchased: - return colors.accentColorGreen; - case CakePayOrderStatus.new_: - case CakePayOrderStatus.paid: - case CakePayOrderStatus.paidPartial: - return colors.accentColorBlue; - case CakePayOrderStatus.pendingPurchase: - case CakePayOrderStatus.purchaseProcessing: - case CakePayOrderStatus.pendingEmail: - case CakePayOrderStatus.expiredButStillPending: - return colors.accentColorYellow; - case CakePayOrderStatus.expired: - case CakePayOrderStatus.failed: - case CakePayOrderStatus.pendingRefund: - case CakePayOrderStatus.refunded: - return colors.textSubtitle1; - } - } - @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; @@ -385,13 +327,7 @@ class _CakePayOrderViewState extends ConsumerState { if (_loading) { return _scaffold( isDesktop: isDesktop, - child: const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + child: const LoadingIndicator(width: 24, height: 24), ); } @@ -412,24 +348,33 @@ class _CakePayOrderViewState extends ConsumerState { final order = _order!; final paymentOptions = order.paymentOptions; - final statusBadge = Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: _statusColor(context, order.status).withValues(alpha: 0.2), - ), - child: Text( - _statusLabel(order.status), - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context)) - .copyWith(color: _statusColor(context, order.status)), - ), - ); - final details = [ - Row(mainAxisAlignment: MainAxisAlignment.end, children: [statusBadge]), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: order.status + .color(Theme.of(context).extension()!) + .withValues(alpha: 0.2), + ), + child: Text( + order.status.label, + style: + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context)) + .copyWith( + color: order.status.color( + Theme.of(context).extension()!, + ), + ), + ), + ), + ], + ), SizedBox(height: isDesktop ? 8 : 6), RoundedWhiteContainer( child: GestureDetector( @@ -727,7 +672,7 @@ class _CakePayOrderViewState extends ConsumerState { const SizedBox(width: 8), Expanded( child: Text( - _statusLabel(status), + status.label, style: (isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index e1fd13513..3f52d5ada 100644 --- a/lib/pages/cakepay/cakepay_orders_view.dart +++ b/lib/pages/cakepay/cakepay_orders_view.dart @@ -10,6 +10,7 @@ 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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'cakepay_order_view.dart'; @@ -44,12 +45,7 @@ class _CakePayOrdersViewState extends State { for (final id in orderIds) { final resp = await CakePayService.instance.client.getOrder(id); if (!resp.hasError && resp.value != null) { - var order = resp.value!; - final override = CakePayService.devStatusOverrides[order.orderId]; - if (override != null) { - order = order.copyWith(status: override); - } - results.add(order); + results.add(resp.value!); } } @@ -193,14 +189,7 @@ class _CakePayOrdersViewState extends State { final content = Stack( children: [ list, - if (_syncing) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_syncing) const LoadingIndicator(width: 24, height: 24), ], ); diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 1a3a88db9..700088664 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -36,6 +36,7 @@ import '../../../widgets/dialogs/basic_dialog.dart'; import '../../../widgets/exchange/trocador/trocador_kyc_info_button.dart'; import '../../../widgets/exchange/trocador/trocador_rating_type_enum.dart'; import '../../../widgets/icon_widgets/exchange_icon.dart'; +import '../../../widgets/loading_indicator.dart'; class ExchangeOption extends ConsumerStatefulWidget { const ExchangeOption({ @@ -388,9 +389,7 @@ class _ProviderOptionState extends ConsumerState { if (loadingProgress == null) { return child; } else { - return const Center( - child: CircularProgressIndicator(), - ); + return const LoadingIndicator(); } }, errorBuilder: (context, error, stackTrace) { diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index ca102c4c5..46ab31b91 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -17,8 +17,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import '../../../db/isar/main_db.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/providers.dart'; -import '../../../services/cakepay/cakepay_service.dart'; -import '../../../services/cakepay/src/models/order.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; @@ -369,25 +367,6 @@ class HiddenSettings extends StatelessWidget { ); }, ), - const SizedBox(height: 12), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (_) => const _CakePayDevStatusDialog(), - ); - }, - child: RoundedWhiteContainer( - child: Text( - "CakePay status overrides", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), - ), - ), // const SizedBox( // height: 12, // ), @@ -428,124 +407,3 @@ class HiddenSettings extends StatelessWidget { ); } } - -class _CakePayDevStatusDialog extends StatefulWidget { - const _CakePayDevStatusDialog(); - - @override - State<_CakePayDevStatusDialog> createState() => - _CakePayDevStatusDialogState(); -} - -class _CakePayDevStatusDialogState extends State<_CakePayDevStatusDialog> { - late final List _orderIds; - - @override - void initState() { - super.initState(); - _orderIds = CakePayService.instance.getOrderIds(); - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).extension()!; - - return AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "CakePay Status Overrides", - style: STextStyles.pageTitleH2(context), - ), - if (CakePayService.devStatusOverrides.isNotEmpty) - TextButton( - onPressed: () { - setState(() { - CakePayService.devStatusOverrides.clear(); - }); - }, - child: Text("Clear all", style: STextStyles.link2(context)), - ), - ], - ), - content: SizedBox( - width: 400, - child: _orderIds.isEmpty - ? Text( - "No tracked CakePay orders.\n" - "Create an order first, then come back here to override " - "its status.", - style: STextStyles.itemSubtitle(context), - ) - : ListView.separated( - shrinkWrap: true, - itemCount: _orderIds.length, - separatorBuilder: (_, __) => const Divider(height: 16), - itemBuilder: (context, index) { - final id = _orderIds[index]; - final current = CakePayService.devStatusOverrides[id]; - - return Row( - children: [ - Expanded( - child: Text( - id.length > 12 ? "${id.substring(0, 12)}..." : id, - style: STextStyles.itemSubtitle12(context), - ), - ), - const SizedBox(width: 8), - DropdownButton( - value: current, - hint: Text( - "API default", - style: STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle2), - ), - underline: const SizedBox(), - isDense: true, - items: [ - DropdownMenuItem( - value: null, - child: Text( - "API default", - style: STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle2), - ), - ), - ...CakePayOrderStatus.values.map( - (s) => DropdownMenuItem( - value: s, - child: Text( - s.value, - style: STextStyles.itemSubtitle12(context), - ), - ), - ), - ], - onChanged: (value) { - setState(() { - if (value == null) { - CakePayService.devStatusOverrides.remove(id); - } else { - CakePayService.devStatusOverrides[id] = value; - } - }); - }, - ), - ], - ); - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close", style: STextStyles.button(context)), - ), - ], - ); - } -} diff --git a/lib/pages/shopinbit/shopinbit_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart index ace2f3d37..f746b03c8 100644 --- a/lib/pages/shopinbit/shopinbit_offer_view.dart +++ b/lib/pages/shopinbit/shopinbit_offer_view.dart @@ -11,6 +11,7 @@ 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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_shipping_view.dart'; @@ -154,14 +155,6 @@ class _ShopInBitOfferViewState extends State { ], ); - const loadingOverlay = Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - if (isDesktop) { return DesktopDialog( maxWidth: 580, @@ -187,7 +180,12 @@ class _ShopInBitOfferViewState extends State { horizontal: 32, vertical: 16, ), - child: Stack(children: [content, if (_loading) loadingOverlay]), + child: Stack( + children: [ + content, + if (_loading) const LoadingIndicator(width: 24, height: 24), + ], + ), ), ), ], @@ -220,7 +218,7 @@ class _ShopInBitOfferViewState extends State { ), ), ), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ); }, diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart index 0467d3fb7..98136696d 100644 --- a/lib/pages/shopinbit/shopinbit_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_payment_view.dart @@ -29,6 +29,7 @@ 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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_send_from_view.dart'; @@ -471,14 +472,6 @@ class _ShopInBitPaymentViewState extends ConsumerState { Widget build(BuildContext context) { final isDesktop = Util.isDesktop; - const loadingOverlay = Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - // Build coin rows from _methods/_addresses final coinRows = []; for (int i = 0; i < _methods.length; i++) { @@ -737,7 +730,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { child: Stack( children: [ SingleChildScrollView(child: content), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ), ), @@ -779,7 +772,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { ), ), ), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ); }, diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 85ceb97cf..1a7781656 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -15,6 +15,7 @@ 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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_offer_view.dart'; @@ -497,14 +498,7 @@ class _ShopInBitTicketDetailState extends State { return _chatBubble(message, isDesktop); }, ), - if (_loading) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ), ); diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index ce62d3be3..d600a00fc 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -15,6 +15,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_car_fee_view.dart'; import 'shopinbit_car_research_payment_view.dart'; @@ -446,14 +447,7 @@ class _ShopInBitTicketsViewState extends State { final content = Stack( children: [ list, - if (_syncing) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_syncing) const LoadingIndicator(width: 24, height: 24), ], ); diff --git a/lib/services/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 1016bc4b7..86c493045 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,16 +1,11 @@ import '../../db/hive/db.dart'; import '../../external_api_keys.dart'; import 'src/client.dart'; -import 'src/models/order.dart'; class CakePayService { static final instance = CakePayService._(); CakePayService._(); - /// Dev-only: override order statuses for local UI testing. - /// Keys are order IDs, values are the status to pretend the API returned. - static final Map devStatusOverrides = {}; - CakePayClient? _client; CakePayClient get client { From 2c790e2a859d52f32d6afa6d25f191826e828c18 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:55:41 -0600 Subject: [PATCH 02/27] feat(db): create a shared drift database with initial table for cake pay order IDs --- lib/db/drift/shared_database.dart | 51 +++++ lib/db/drift/shared_database.g.dart | 303 ++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 lib/db/drift/shared_database.dart create mode 100644 lib/db/drift/shared_database.g.dart diff --git a/lib/db/drift/shared_database.dart b/lib/db/drift/shared_database.dart new file mode 100644 index 000000000..2e00d3c90 --- /dev/null +++ b/lib/db/drift/shared_database.dart @@ -0,0 +1,51 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as path; + +import '../../utilities/stack_file_system.dart'; + +part 'shared_database.g.dart'; + +abstract final class SharedDrift { + static bool _didInit = false; + + static SharedDatabase? _db; + + static SharedDatabase get() { + if (!_didInit) { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + _didInit = true; + } + + return _db ??= SharedDatabase._(); + } +} + +class CakepayOrders extends Table { + TextColumn get orderId => text()(); + + @override + Set get primaryKey => {orderId}; +} + +@DriftDatabase(tables: [CakepayOrders]) +final class SharedDatabase extends _$SharedDatabase { + SharedDatabase._([QueryExecutor? executor]) + : super(executor ?? _openConnection()); + + @override + int get schemaVersion => 1; + + static QueryExecutor _openConnection() { + return driftDatabase( + name: "shared", + native: DriftNativeOptions( + shareAcrossIsolates: true, + databasePath: () async { + final dir = await StackFileSystem.applicationDriftDirectory(); + return path.join(dir.path, "shared", "shared.db"); + }, + ), + ); + } +} diff --git a/lib/db/drift/shared_database.g.dart b/lib/db/drift/shared_database.g.dart new file mode 100644 index 000000000..2e0c5de8b --- /dev/null +++ b/lib/db/drift/shared_database.g.dart @@ -0,0 +1,303 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shared_database.dart'; + +// ignore_for_file: type=lint +class $CakepayOrdersTable extends CakepayOrders + with TableInfo<$CakepayOrdersTable, CakepayOrder> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CakepayOrdersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _orderIdMeta = const VerificationMeta( + 'orderId', + ); + @override + late final GeneratedColumn orderId = GeneratedColumn( + 'order_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [orderId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'cakepay_orders'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('order_id')) { + context.handle( + _orderIdMeta, + orderId.isAcceptableOrUnknown(data['order_id']!, _orderIdMeta), + ); + } else if (isInserting) { + context.missing(_orderIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {orderId}; + @override + CakepayOrder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CakepayOrder( + orderId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_id'], + )!, + ); + } + + @override + $CakepayOrdersTable createAlias(String alias) { + return $CakepayOrdersTable(attachedDatabase, alias); + } +} + +class CakepayOrder extends DataClass implements Insertable { + final String orderId; + const CakepayOrder({required this.orderId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['order_id'] = Variable(orderId); + return map; + } + + CakepayOrdersCompanion toCompanion(bool nullToAbsent) { + return CakepayOrdersCompanion(orderId: Value(orderId)); + } + + factory CakepayOrder.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CakepayOrder(orderId: serializer.fromJson(json['orderId'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'orderId': serializer.toJson(orderId)}; + } + + CakepayOrder copyWith({String? orderId}) => + CakepayOrder(orderId: orderId ?? this.orderId); + CakepayOrder copyWithCompanion(CakepayOrdersCompanion data) { + return CakepayOrder( + orderId: data.orderId.present ? data.orderId.value : this.orderId, + ); + } + + @override + String toString() { + return (StringBuffer('CakepayOrder(') + ..write('orderId: $orderId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => orderId.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CakepayOrder && other.orderId == this.orderId); +} + +class CakepayOrdersCompanion extends UpdateCompanion { + final Value orderId; + final Value rowid; + const CakepayOrdersCompanion({ + this.orderId = const Value.absent(), + this.rowid = const Value.absent(), + }); + CakepayOrdersCompanion.insert({ + required String orderId, + this.rowid = const Value.absent(), + }) : orderId = Value(orderId); + static Insertable custom({ + Expression? orderId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (orderId != null) 'order_id': orderId, + if (rowid != null) 'rowid': rowid, + }); + } + + CakepayOrdersCompanion copyWith({Value? orderId, Value? rowid}) { + return CakepayOrdersCompanion( + orderId: orderId ?? this.orderId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (orderId.present) { + map['order_id'] = Variable(orderId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CakepayOrdersCompanion(') + ..write('orderId: $orderId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$SharedDatabase extends GeneratedDatabase { + _$SharedDatabase(QueryExecutor e) : super(e); + $SharedDatabaseManager get managers => $SharedDatabaseManager(this); + late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [cakepayOrders]; +} + +typedef $$CakepayOrdersTableCreateCompanionBuilder = + CakepayOrdersCompanion Function({ + required String orderId, + Value rowid, + }); +typedef $$CakepayOrdersTableUpdateCompanionBuilder = + CakepayOrdersCompanion Function({Value orderId, Value rowid}); + +class $$CakepayOrdersTableFilterComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CakepayOrdersTableOrderingComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CakepayOrdersTableAnnotationComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get orderId => + $composableBuilder(column: $table.orderId, builder: (column) => column); +} + +class $$CakepayOrdersTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + > { + $$CakepayOrdersTableTableManager( + _$SharedDatabase db, + $CakepayOrdersTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CakepayOrdersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CakepayOrdersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CakepayOrdersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value orderId = const Value.absent(), + Value rowid = const Value.absent(), + }) => CakepayOrdersCompanion(orderId: orderId, rowid: rowid), + createCompanionCallback: + ({ + required String orderId, + Value rowid = const Value.absent(), + }) => + CakepayOrdersCompanion.insert(orderId: orderId, rowid: rowid), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CakepayOrdersTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + >; + +class $SharedDatabaseManager { + final _$SharedDatabase _db; + $SharedDatabaseManager(this._db); + $$CakepayOrdersTableTableManager get cakepayOrders => + $$CakepayOrdersTableTableManager(_db, _db.cakepayOrders); +} From e37737e8140467d3593b58d4290c40d6449354a8 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:57:16 -0600 Subject: [PATCH 03/27] use drift/sqlite to store order ids instead of piggybacking in the preferences store --- lib/pages/cakepay/cakepay_orders_view.dart | 2 +- lib/services/cakepay/cakepay_service.dart | 52 ++++++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index 3f52d5ada..48b966507 100644 --- a/lib/pages/cakepay/cakepay_orders_view.dart +++ b/lib/pages/cakepay/cakepay_orders_view.dart @@ -39,7 +39,7 @@ class _CakePayOrdersViewState extends State { Future _syncFromApi() async { setState(() => _syncing = true); try { - final orderIds = CakePayService.instance.getOrderIds(); + final orderIds = await CakePayService.instance.getOrderIds(); final results = []; for (final id in orderIds) { diff --git a/lib/services/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 86c493045..48db6a917 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,4 +1,6 @@ -import '../../db/hive/db.dart'; +import 'package:drift/drift.dart'; + +import '../../db/drift/shared_database.dart'; import '../../external_api_keys.dart'; import 'src/client.dart'; @@ -12,35 +14,29 @@ class CakePayService { return _client ??= CakePayClient(apiToken: kCakePayApiToken); } - // Mirrors ShopInBit's local ticket storage pattern but uses lightweight - // Hive prefs instead of a full Isar collection, since CakePay orders can - // be fetched individually via getOrder() with the seller key. - - static const _kCakePayOrderIds = "cakePayOrderIds"; - - /// Persist a newly-created order ID so the orders list view can find it - /// later without requiring Knox user auth. - void addOrderId(String orderId) { - final ids = getOrderIds(); - if (!ids.contains(orderId)) { - ids.insert(0, orderId); - DB.instance.put( - boxName: DB.boxNamePrefs, - key: _kCakePayOrderIds, - value: ids, - ); - } + Future addOrderId(String orderId) async { + final db = SharedDrift.get(); + + await db.transaction(() async { + await db + .into(db.cakepayOrders) + .insert( + CakepayOrdersCompanion.insert(orderId: orderId), + mode: .insertOrIgnore, + ); + }); } /// Return locally-tracked order IDs (most recent first). - List getOrderIds() { - final raw = DB.instance.get( - boxName: DB.boxNamePrefs, - key: _kCakePayOrderIds, - ); - if (raw is List) { - return raw.cast().toList(); - } - return []; + Future> getOrderIds() async { + final db = SharedDrift.get(); + + final rows = + await (db.select(db.cakepayOrders)..orderBy([ + (t) => OrderingTerm(expression: t.rowId, mode: OrderingMode.desc), + ])) + .get(); + + return rows.map((row) => row.orderId).toList(); } } From cdcc72d6d8995fc15ea2603ac8297a55544f5906 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 May 2026 14:24:51 -0600 Subject: [PATCH 04/27] chore: refactor a big widget's build method and some more general house keeping --- .../cakepay/cakepay_card_detail_view.dart | 749 ++++++++++-------- lib/pages/cakepay/cakepay_vendors_view.dart | 50 +- lib/pages/more_view/gift_cards_view.dart | 29 +- lib/pages/wallet_view/wallet_view.dart | 8 +- .../sub_widgets/desktop_gift_cards_view.dart | 17 +- lib/services/cakepay/src/models/card.dart | 110 +-- .../icon_widgets/credit_card_icon.dart | 31 + 7 files changed, 588 insertions(+), 406 deletions(-) create mode 100644 lib/widgets/icon_widgets/credit_card_icon.dart diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 7fbd0ebff..371d61e1c 100644 --- a/lib/pages/cakepay/cakepay_card_detail_view.dart +++ b/lib/pages/cakepay/cakepay_card_detail_view.dart @@ -1,3 +1,4 @@ +import 'package:decimal/decimal.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -15,6 +16,7 @@ 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/icon_widgets/credit_card_icon.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; @@ -34,7 +36,7 @@ class CakePayCardDetailView extends StatefulWidget { class _CakePayCardDetailViewState extends State { late CakePayCard _card; bool _purchasing = false; - double? _selectedDenomination; + Decimal? _selectedDenomination; int _quantity = 1; bool _termsAccepted = false; final _customAmountController = TextEditingController(); @@ -75,8 +77,8 @@ class _CakePayCardDetailViewState extends State { if (_emailController.text.trim().isEmpty) return false; final price = _priceString; if (price.isEmpty) return false; - final parsed = double.tryParse(price); - if (parsed == null || parsed <= 0) return false; + final parsed = Decimal.tryParse(price); + if (parsed == null || parsed <= Decimal.zero) return false; if (_card.isRangeDenomination) { if (_card.minValue != null && parsed < _card.minValue!) return false; if (_card.maxValue != null && parsed > _card.maxValue!) return false; @@ -202,19 +204,21 @@ class _CakePayCardDetailViewState extends State { // Track order ID locally so the orders list view can fetch it // via getOrder() without requiring Knox user auth. - CakePayService.instance.addOrderId(order.orderId); + await CakePayService.instance.addOrderId(order.orderId); - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - await showDialog( - context: context, - builder: (_) => CakePayOrderView(orderId: order.orderId), - ); - } else { - await Navigator.of(context).pushReplacementNamed( - CakePayOrderView.routeName, - arguments: order.orderId, - ); + if (mounted) { + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + await showDialog( + context: context, + builder: (_) => CakePayOrderView(orderId: order.orderId), + ); + } else { + await Navigator.of(context).pushReplacementNamed( + CakePayOrderView.routeName, + arguments: order.orderId, + ); + } } } else { await showDialog( @@ -251,90 +255,366 @@ class _CakePayCardDetailViewState extends State { final isDesktop = Util.isDesktop; final card = _card; - final denominationSelector = card.isFixedDenomination - ? Wrap( - spacing: 8, - runSpacing: 8, - children: card.denominations.map((d) { - final selected = d == _selectedDenomination; - return ChoiceChip( - label: Text( - "${d.toStringAsFixed(0)} ${card.currencyCode ?? ''}", - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context)) - .copyWith( - color: selected - ? Theme.of( - context, - ).extension()!.textDark - : null, - ), + 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), + ), ), - selected: selected, - onSelected: (val) { - if (val) setState(() => _selectedDenomination = d); - }, - ); - }).toList(), - ) - : card.isRangeDenomination - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 8, + ), + child: child, + ), + ), + ], + ), + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text("Gift Card", style: STextStyles.navBarTitle(context)), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: child, + ), + ), + ), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (card.cardImageUrl != null) + _CardImage(imageUrl: card.cardImageUrl!, isDesktop: isDesktop), + 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(() {}), + ), + 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), + onOpenTerms: _openTerms, + ), + SizedBox(height: isDesktop ? 16 : 12), Text( - "Enter amount (${card.minValue?.toStringAsFixed(0) ?? '?'} - " - "${card.maxValue?.toStringAsFixed(0) ?? '?'} " - "${card.currencyCode ?? ''})", + "Email for receipt and delivery", 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: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ), - decoration: - standardInputDecoration( - "Amount", - _customAmountFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - ), + _EmailField( + 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), ], - ) - : const SizedBox.shrink(); + ), + ), + ), + ); + } +} + +class _CardImage extends StatelessWidget { + const _CardImage({required this.imageUrl, required this.isDesktop}); + + final String imageUrl; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + imageUrl, + width: isDesktop ? 200 : 150, + fit: BoxFit.contain, + errorBuilder: (BuildContext _, Object __, StackTrace? ___) => + CreditCardIcon( + width: isDesktop ? 80 : 60, + height: isDesktop ? 80 : 60, + ), + ), + ), + ); + } +} + +class _PlainInfoBlock extends StatelessWidget { + const _PlainInfoBlock({required this.text, required this.isDesktop}); + + final String text; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Text( + text, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context), + ), + ); + } +} + +class _TitledInfoBlock extends StatelessWidget { + const _TitledInfoBlock({ + required this.title, + required this.body, + required this.isDesktop, + }); + + final String title; + final String body; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), + ), + const SizedBox(height: 8), + Text( + body, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + ); + } +} + +class _DenominationSelector extends StatelessWidget { + const _DenominationSelector({ + required this.card, + required this.isDesktop, + required this.selectedDenomination, + required this.customAmountController, + required this.customAmountFocusNode, + required this.onDenominationSelected, + required this.onCustomAmountChanged, + }); + + final CakePayCard card; + final bool isDesktop; + final Decimal? selectedDenomination; + final TextEditingController customAmountController; + final FocusNode customAmountFocusNode; + final ValueChanged onDenominationSelected; + final VoidCallback onCustomAmountChanged; + + @override + Widget build(BuildContext context) { + if (card.isFixedDenomination) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: card.denominations.map((d) { + final bool selected = d == selectedDenomination; + return ChoiceChip( + label: Text( + "${d.toStringAsFixed(0)} ${card.currencyCode ?? ''}", + style: + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context)) + .copyWith( + color: selected + ? Theme.of( + context, + ).extension()!.textDark + : null, + ), + ), + selected: selected, + onSelected: (bool val) { + if (val) onDenominationSelected(d); + }, + ); + }).toList(), + ); + } + + if (card.isRangeDenomination) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter amount (${card.minValue?.toStringAsFixed(0) ?? '?'} - " + "${card.maxValue?.toStringAsFixed(0) ?? '?'} " + "${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()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveText, + ), + decoration: + standardInputDecoration( + "Amount", + customAmountFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + filled: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + } +} - final quantityRow = Row( +class _QuantityRow extends StatelessWidget { + const _QuantityRow({ + required this.isDesktop, + required this.quantity, + required this.onDecrement, + required this.onIncrement, + }); + + final bool isDesktop; + final int quantity; + final VoidCallback? onDecrement; + final VoidCallback onIncrement; + + @override + Widget build(BuildContext context) { + return Row( children: [ Text( "Quantity", @@ -345,23 +625,40 @@ class _CakePayCardDetailViewState extends State { const Spacer(), IconButton( icon: const Icon(Icons.remove_circle_outline, size: 20), - onPressed: _quantity > 1 ? () => setState(() => _quantity--) : null, + onPressed: onDecrement, ), Text( - "$_quantity", + "$quantity", style: isDesktop ? STextStyles.desktopTextSmall(context) : STextStyles.titleBold12(context), ), IconButton( icon: const Icon(Icons.add_circle_outline, size: 20), - onPressed: () => setState(() => _quantity++), + onPressed: onIncrement, ), ], ); + } +} + +class _TermsCheckbox extends StatelessWidget { + const _TermsCheckbox({ + required this.isDesktop, + required this.accepted, + required this.onToggle, + required this.onOpenTerms, + }); + + final bool isDesktop; + final bool accepted; + final VoidCallback onToggle; + final VoidCallback onOpenTerms; - final termsCheckbox = GestureDetector( - onTap: () => setState(() => _termsAccepted = !_termsAccepted), + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onToggle, child: Container( color: Colors.transparent, child: Row( @@ -373,7 +670,7 @@ class _CakePayCardDetailViewState extends State { child: IgnorePointer( child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _termsAccepted, + value: accepted, onChanged: (_) {}, ), ), @@ -392,7 +689,7 @@ class _CakePayCardDetailViewState extends State { style: STextStyles.richLink( context, ).copyWith(fontSize: isDesktop ? null : 14), - recognizer: TapGestureRecognizer()..onTap = _openTerms, + recognizer: TapGestureRecognizer()..onTap = onOpenTerms, ), const TextSpan( text: @@ -410,230 +707,58 @@ class _CakePayCardDetailViewState extends State { ), ), ); + } +} - final content = SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (card.cardImageUrl != null) - Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - card.cardImageUrl!, - width: isDesktop ? 200 : 150, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => - Icon(Icons.card_giftcard, size: isDesktop ? 80 : 60), - ), - ), - ), - 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), - RoundedWhiteContainer( - child: Text( - card.description!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - ), - ], - if (card.howToUse != null && card.howToUse!.isNotEmpty) ...[ - SizedBox(height: isDesktop ? 16 : 12), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "How to use", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - const SizedBox(height: 8), - Text( - card.howToUse!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - ], - if (card.termsAndConditions != null && - card.termsAndConditions!.isNotEmpty) ...[ - SizedBox(height: isDesktop ? 16 : 12), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Terms & conditions", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - const SizedBox(height: 8), - Text( - card.termsAndConditions!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - ], - if (card.expiryAndValidity != null && - card.expiryAndValidity!.isNotEmpty) ...[ - SizedBox(height: isDesktop ? 16 : 12), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Expiry & validity", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - const SizedBox(height: 8), - Text( - card.expiryAndValidity!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - ], - SizedBox(height: isDesktop ? 24 : 16), - denominationSelector, - SizedBox(height: isDesktop ? 16 : 12), - quantityRow, - SizedBox(height: isDesktop ? 16 : 12), - termsCheckbox, - SizedBox(height: isDesktop ? 16 : 12), - Text( - "Email for receipt and delivery", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _emailController, - focusNode: _emailFocusNode, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.emailAddress, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ), - decoration: - standardInputDecoration( - "Email", - _emailFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - PrimaryButton( - label: _purchasing ? "Processing..." : "Purchase", - enabled: _canPurchase, - onPressed: _canPurchase ? _purchase : null, - ), - ], - ), - ); +class _EmailField extends StatelessWidget { + const _EmailField({ + required this.isDesktop, + required this.controller, + required this.focusNode, + required this.onChanged, + }); - return _scaffold(isDesktop: isDesktop, child: content); - } + final bool isDesktop; + final TextEditingController controller; + final FocusNode focusNode; + final VoidCallback onChanged; - Widget _scaffold({required bool isDesktop, required Widget child}) { - 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), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 8, - ), - child: child, + @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()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveText, ), - ), - ], - ), - ), - child: ConditionalParent( - condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: Theme.of( + decoration: + standardInputDecoration( + "Email", + focusNode, context, - ).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () => Navigator.of(context).pop(), + desktopMed: isDesktop, + ).copyWith( + filled: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - title: Text("Gift Card", style: STextStyles.navBarTitle(context)), ), - body: SafeArea( - child: Padding(padding: const EdgeInsets.all(16), child: child), - ), - ), - ), - child: child, ), ); } diff --git a/lib/pages/cakepay/cakepay_vendors_view.dart b/lib/pages/cakepay/cakepay_vendors_view.dart index 2c7b3f5cb..5c16cdd7d 100644 --- a/lib/pages/cakepay/cakepay_vendors_view.dart +++ b/lib/pages/cakepay/cakepay_vendors_view.dart @@ -15,6 +15,7 @@ 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/icon_widgets/credit_card_icon.dart'; import '../../widgets/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_text_field.dart'; @@ -96,15 +97,19 @@ class _CakePayVendorsViewState extends State { }); } - void _onCardTapped(CakePayCard card) { + Future _onCardTapped(CakePayCard card) async { if (Util.isDesktop) { + // this pop makes going back annoying as the whole list needs to be + // searched again with API calls etc. Leaving in for now as this is how I + // found it and removing here could introduce worse issues somewhere else. Navigator.of(context, rootNavigator: true).pop(); - showDialog( + + await showDialog( context: context, builder: (_) => CakePayCardDetailView(card: card), ); } else { - Navigator.of( + await Navigator.of( context, ).pushNamed(CakePayCardDetailView.routeName, arguments: card); } @@ -165,7 +170,10 @@ class _CakePayVendorsViewState extends State { ), ), body: SafeArea( - child: Padding(padding: const EdgeInsets.all(16), child: child), + child: Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: child, + ), ), ), ), @@ -205,6 +213,9 @@ class _CakePayVendorsViewState extends State { shrinkWrap: isDesktop, primary: isDesktop ? false : null, itemCount: cards.length, + padding: isDesktop + ? null + : const EdgeInsets.only(bottom: 16), separatorBuilder: (_, __) => SizedBox(height: isDesktop ? 16 : 12), itemBuilder: (_, index) => _CardTile( @@ -256,9 +267,16 @@ class _SearchField extends StatelessWidget { focusNode, context, ).copyWith( - prefixIcon: const Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 12), - child: Icon(Icons.search, size: 20), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), ), onSubmitted: onSubmitted, @@ -411,10 +429,15 @@ class _CardTile extends StatelessWidget { width: isDesktop ? 60 : 48, height: isDesktop ? 40 : 32, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - Icon(Icons.card_giftcard, size: isDesktop ? 40 : 32), + errorBuilder: (_, __, ___) => CreditCardIcon( + width: isDesktop ? 40 : 32, + height: isDesktop ? 40 : 32, + ), ) - : Icon(Icons.card_giftcard, size: isDesktop ? 40 : 32), + : CreditCardIcon( + width: isDesktop ? 40 : 32, + height: isDesktop ? 40 : 32, + ), ), const SizedBox(width: 12), Expanded( @@ -445,7 +468,12 @@ class _CardTile extends StatelessWidget { ], ), ), - Icon(Icons.chevron_right, color: colors.textSubtitle1), + SvgPicture.asset( + Assets.svg.chevronRight, + width: 20, + height: 20, + colorFilter: ColorFilter.mode(colors.textSubtitle1, .srcIn), + ), ], ), ), diff --git a/lib/pages/more_view/gift_cards_view.dart b/lib/pages/more_view/gift_cards_view.dart index 9fcf82bbc..48ff0f364 100644 --- a/lib/pages/more_view/gift_cards_view.dart +++ b/lib/pages/more_view/gift_cards_view.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../services/tor_service.dart'; import '../../themes/stack_colors.dart'; -import '../../utilities/assets.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/desktop/secondary_button.dart'; +import '../../widgets/icon_widgets/credit_card_icon.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/tor_subscription.dart'; import '../cakepay/cakepay_orders_view.dart'; @@ -51,11 +50,7 @@ class _GiftCardsViewState extends ConsumerState { context, ).extension()!.background, appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), + leading: const AppBarBackButton(), title: Text("Gift cards", style: STextStyles.navBarTitle(context)), ), body: SafeArea( @@ -69,11 +64,7 @@ class _GiftCardsViewState extends ConsumerState { children: [ Row( children: [ - SvgPicture.asset( - Assets.svg.creditCard, - width: 32, - height: 32, - ), + const CreditCardIcon(width: 32, height: 32), const SizedBox(width: 12), Expanded( child: Column( @@ -116,24 +107,26 @@ class _GiftCardsViewState extends ConsumerState { Row( children: [ Expanded( - child: PrimaryButton( - label: "Browse", + child: SecondaryButton( + label: "My Orders", enabled: !_torEnabled, onPressed: () { Navigator.of( context, - ).pushNamed(CakePayVendorsView.routeName); + ).pushNamed(CakePayOrdersView.routeName); }, ), ), + const SizedBox(width: 16), Expanded( - child: SecondaryButton( - label: "My Orders", + child: PrimaryButton( + label: "Browse", + enabled: !_torEnabled, onPressed: () { Navigator.of( context, - ).pushNamed(CakePayOrdersView.routeName); + ).pushNamed(CakePayVendorsView.routeName); }, ), ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 12affd42b..83a4d6e8f 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -71,6 +71,7 @@ import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_loading_overlay.dart'; import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/frost_scaffold.dart'; +import '../../widgets/icon_widgets/credit_card_icon.dart'; import '../../widgets/loading_indicator.dart'; import '../../widgets/small_tor_icon.dart'; import '../../widgets/stack_dialog.dart'; @@ -96,6 +97,8 @@ import '../exchange_view/wallet_initiated_exchange_view.dart'; import '../finalize_view/finalize_view.dart'; import '../masternodes/masternodes_home_view.dart'; import '../monkey/monkey_view.dart'; +import '../more_view/gift_cards_view.dart'; +import '../more_view/services_view.dart'; import '../namecoin_names/namecoin_names_home_view.dart'; import '../notification_views/notifications_view.dart'; import '../ordinals/ordinals_view.dart'; @@ -109,8 +112,6 @@ import '../settings_views/wallet_settings_view/wallet_network_settings_view/wall import '../settings_views/wallet_settings_view/wallet_settings_view.dart'; import '../signing/signing_view.dart'; import '../spark_names/spark_names_home_view.dart'; -import '../more_view/gift_cards_view.dart'; -import '../more_view/services_view.dart'; import '../token_view/my_tokens_view.dart'; import 'sub_widgets/transactions_list.dart'; import 'sub_widgets/wallet_summary.dart'; @@ -1364,8 +1365,7 @@ class _WalletViewState extends ConsumerState { ), WalletNavigationBarItemData( label: "Gift cards", - icon: SvgPicture.asset( - Assets.svg.creditCard, + icon: CreditCardIcon( height: 20, width: 20, color: Theme.of( diff --git a/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart b/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart index 7693f4357..964028acb 100644 --- a/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart +++ b/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import '../../../app_config.dart'; import '../../../pages/cakepay/cakepay_orders_view.dart'; @@ -8,10 +7,10 @@ import '../../../pages/cakepay/cakepay_vendors_view.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/tor_service.dart'; import '../../../themes/stack_colors.dart'; -import '../../../utilities/assets.dart'; import '../../../utilities/text_styles.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/icon_widgets/credit_card_icon.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/tor_subscription.dart'; @@ -53,17 +52,9 @@ class _DesktopGiftCardsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - Assets.svg.creditCard, - width: 48, - height: 48, - colorFilter: ColorFilter.mode( - Theme.of(context).extension()!.textDark, - BlendMode.srcIn, - ), - ), + const Padding( + padding: EdgeInsets.all(8.0), + child: CreditCardIcon(width: 48, height: 48), ), Padding( padding: const EdgeInsets.all(10), diff --git a/lib/services/cakepay/src/models/card.dart b/lib/services/cakepay/src/models/card.dart index 2fed2f47e..83d2eb3bc 100644 --- a/lib/services/cakepay/src/models/card.dart +++ b/lib/services/cakepay/src/models/card.dart @@ -1,3 +1,5 @@ +import "package:decimal/decimal.dart"; + class CakePayCard { final int id; final String name; @@ -9,11 +11,11 @@ class CakePayCard { final String? cardImageUrl; final String? country; final String? currencyCode; - final List denominations; - final double? minValue; - final double? maxValue; - final double? minValueUsd; - final double? maxValueUsd; + final List denominations; + final Decimal? minValue; + final Decimal? maxValue; + final Decimal? minValueUsd; + final Decimal? maxValueUsd; final bool available; final String? lastUpdated; @@ -38,72 +40,84 @@ class CakePayCard { }); factory CakePayCard.fromJson(Map json) { - final rawDenoms = json['denominations'] ?? json['denominations_list']; - final denominations = []; + final dynamic rawDenoms = + json["denominations"] ?? json["denominations_list"]; + final List denominations = []; if (rawDenoms is List) { - for (final d in rawDenoms) { - if (d is num) { - denominations.add(d.toDouble()); - } else if (d is String) { - final parsed = double.tryParse(d); - if (parsed != null) denominations.add(parsed); - } else if (d is Map) { - final v = d['value']; - if (v is num) { - denominations.add(v.toDouble()); - } else if (v is String) { - final parsed = double.tryParse(v); - if (parsed != null) denominations.add(parsed); - } - } + for (final dynamic d in rawDenoms) { + final Decimal? parsed = _toDecimal(d is Map ? d["value"] : d); + if (parsed != null) denominations.add(parsed); } } return CakePayCard( - id: json['id'] as int? ?? 0, - name: (json['name'] ?? '') as String, - type: json['type'] as String?, - description: json['description'] as String?, - termsAndConditions: json['terms_and_conditions'] as String?, - howToUse: json['how_to_use'] as String?, - expiryAndValidity: json['expiry_and_validity'] as String?, - cardImageUrl: json['card_image_url'] as String?, - country: json['country'] is Map - ? (json['country'] as Map)['name'] as String? - : json['country'] as String?, - currencyCode: json['currency_code'] as String?, + id: json["id"] as int? ?? 0, + name: (json["name"] ?? "") as String, + type: json["type"] as String?, + description: json["description"] as String?, + termsAndConditions: json["terms_and_conditions"] as String?, + howToUse: json["how_to_use"] as String?, + expiryAndValidity: json["expiry_and_validity"] as String?, + cardImageUrl: json["card_image_url"] as String?, + country: json["country"] is Map + ? (json["country"] as Map)["name"] as String? + : json["country"] as String?, + currencyCode: json["currency_code"] as String?, denominations: denominations, - minValue: _toDouble(json['min_value']), - maxValue: _toDouble(json['max_value']), - minValueUsd: _toDouble(json['min_value_usd']), - maxValueUsd: _toDouble(json['max_value_usd']), - available: json['available'] as bool? ?? true, - lastUpdated: json['last_updated'] as String?, + minValue: _toDecimal(json["min_value"]), + maxValue: _toDecimal(json["max_value"]), + minValueUsd: _toDecimal(json["min_value_usd"]), + maxValueUsd: _toDecimal(json["max_value_usd"]), + available: json["available"] as bool? ?? true, + lastUpdated: json["last_updated"] as String?, ); } + Map toMap() { + return { + "id": id, + "name": name, + "type": type, + "description": description, + "terms_and_conditions": termsAndConditions, + "how_to_use": howToUse, + "expiry_and_validity": expiryAndValidity, + "card_image_url": cardImageUrl, + "country": country, + "currency_code": currencyCode, + "denominations": denominations.map((Decimal d) => d.toString()).toList(), + "min_value": minValue?.toString(), + "max_value": maxValue?.toString(), + "min_value_usd": minValueUsd?.toString(), + "max_value_usd": maxValueUsd?.toString(), + "available": available, + "last_updated": lastUpdated, + }; + } + bool get isFixedDenomination => denominations.isNotEmpty; bool get isRangeDenomination => denominations.isEmpty && minValue != null && maxValue != null; String get denominationRange { if (isFixedDenomination) { - return denominations.map((d) => d.toStringAsFixed(0)).join(', '); + return denominations.map((Decimal d) => d.toStringAsFixed(0)).join(", "); } if (isRangeDenomination) { - return '${minValue!.toStringAsFixed(0)} - ${maxValue!.toStringAsFixed(0)}'; + return "${minValue!.toStringAsFixed(0)} - ${maxValue!.toStringAsFixed(0)}"; } - return ''; + return ""; } @override - String toString() => 'CakePayCard($id, $name)'; + String toString() => toMap().toString(); } -double? _toDouble(dynamic v) { +Decimal? _toDecimal(dynamic v) { if (v == null) return null; - if (v is double) return v; - if (v is int) return v.toDouble(); - if (v is String) return double.tryParse(v); + if (v is Decimal) return v; + if (v is int) return Decimal.fromInt(v); + if (v is double) return Decimal.parse(v.toString()); + if (v is String) return Decimal.tryParse(v); return null; } diff --git a/lib/widgets/icon_widgets/credit_card_icon.dart b/lib/widgets/icon_widgets/credit_card_icon.dart new file mode 100644 index 000000000..369792e56 --- /dev/null +++ b/lib/widgets/icon_widgets/credit_card_icon.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; + +class CreditCardIcon extends StatelessWidget { + const CreditCardIcon({ + super.key, + this.width = 32, + this.height = 32, + this.color, + }); + + final double width; + final double height; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.creditCard, + width: width, + height: height, + colorFilter: ColorFilter.mode( + color ?? Theme.of(context).extension()!.textDark3, + BlendMode.srcIn, + ), + ); + } +} From 211971b5d7c6cac19173071026388dbaf756e8ab Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 May 2026 14:27:01 -0600 Subject: [PATCH 05/27] fix: context.mounted check (and a bunch of auto format) --- .../sub_widgets/crypto_selection_view.dart | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart index 089b492af..7f7705b43 100644 --- a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart @@ -98,8 +98,9 @@ class _CryptoSelectionViewState extends ConsumerState { builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -109,7 +110,7 @@ class _CryptoSelectionViewState extends ConsumerState { const Duration(milliseconds: 50), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -145,45 +146,45 @@ class _CryptoSelectionViewState extends ConsumerState { focusNode: _searchFocusNode, onChanged: filter, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: - _searchController.text.isNotEmpty + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - filter(""); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 10), @@ -226,14 +227,12 @@ class _CryptoSelectionViewState extends ConsumerState { const SizedBox(height: 2), Text( _coins[index].ticker.toUpperCase(), - style: STextStyles.smallMed12( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) .extension()! .textSubtitle1, - ), + ), ), ], ), From 5eaf945e523d6283c82c293898e8f9ddaa6043aa Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 15 May 2026 14:39:42 -0600 Subject: [PATCH 06/27] chore: fix and clean up WIP --- .../cakepay/cakepay_card_detail_view.dart | 407 +++++++----------- lib/pages/shopinbit/shopinbit_step_1.dart | 250 +++++------ lib/pages/shopinbit/shopinbit_step_2.dart | 398 ++++++++--------- .../sub_widgets/desktop_shopinbit_view.dart | 39 +- lib/widgets/dialogs/s_dialog.dart | 20 +- .../textfields/adaptive_text_field.dart | 4 + 6 files changed, 494 insertions(+), 624 deletions(-) diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 371d61e1c..07ec3d603 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 { 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 { 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 { } Future _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 { 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 { } } } 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( 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()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.buttonTextSecondary, - ), - ), - onPressed: () => Navigator.of(context).pop(), - ), + message: errorMessage, + maxWidth: Util.isDesktop ? 580 : null, + desktopPopRootNavigator: Util.isDesktop, ); }, ); @@ -250,6 +232,22 @@ class _CakePayCardDetailViewState extends State { } } + @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 { 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 { 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 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()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.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 6e6a097c4..9a975aeb1 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 { 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 { } } + @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()!.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()!.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()!.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 9df909ef5..79d6a20ec 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 { 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 { } } - 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()!.textDark - : Theme.of(context).extension()!.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()!.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()!.textDark.withOpacity(0.1), - ), - alignment: Alignment.center, - child: SvgPicture.asset( - iconAsset, - width: isDesktop ? 24 : 20, - height: isDesktop ? 24 : 20, - color: Theme.of(context).extension()!.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()!.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()!.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()!.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 onTap; @override Widget build(BuildContext context) { + final StackColors colors = Theme.of(context).extension()!; 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()!.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 e5c9e596a..010adc5a0 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 { return shouldContinue ?? false; } - void _showShopDialog(BuildContext context) async { + Future _showShopDialog(BuildContext context) async { final service = ShopInBitService.instance; final model = ShopInBitOrderModel(); bool isFirstRun = false; @@ -111,12 +110,12 @@ class _DesktopServicesViewState extends ConsumerState { } } - 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( + await showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => DesktopDialog( @@ -144,36 +143,7 @@ class _DesktopServicesViewState extends ConsumerState { ), 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( - 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 { 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 a6b32148c..6bf66ecf4 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()!.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 e57746a80..da30057e8 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? suffixIcons; + final TextInputType? keyboardType; + @override State createState() => _AdaptiveTextFieldState(); } @@ -112,6 +115,7 @@ class _AdaptiveTextFieldState extends State { autocorrect: widget.autocorrect, enableSuggestions: widget.enableSuggestions, onSubmitted: widget.onSubmitted, + keyboardType: widget.keyboardType, decoration: standardInputDecoration( widget.labelText, From d512c5851ddf433bca90ff4160bceb89ee0f7fb9 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 15 May 2026 16:11:43 -0600 Subject: [PATCH 07/27] fix(ui): dynamic dialog height --- lib/pages/cakepay/cakepay_order_view.dart | 50 ++++++++++++----------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart index aa11d295a..62fc0bd41 100644 --- a/lib/pages/cakepay/cakepay_order_view.dart +++ b/lib/pages/cakepay/cakepay_order_view.dart @@ -20,9 +20,9 @@ import '../../wallets/crypto_currency/crypto_currency.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/loading_indicator.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; @@ -882,31 +882,33 @@ class _CakePayOrderViewState extends ConsumerState { Widget _scaffold({required bool isDesktop, required Widget child}) { return ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: 650, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text("Order", style: STextStyles.desktopH3(context)), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 8, + 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("Order", style: STextStyles.desktopH3(context)), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 8, + ), + child: child, ), - child: child, ), - ), - ], + ], + ), ), ), child: ConditionalParent( From eba532503c24591653fc1c343bad5486d99f1428 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 May 2026 13:38:50 -0600 Subject: [PATCH 08/27] fix(ui): desktop shopinbit dialog flow nested navigation --- lib/pages/shopinbit/shopinbit_step_1.dart | 15 +- lib/pages/shopinbit/shopinbit_step_2.dart | 103 ++++------- lib/pages/shopinbit/shopinbit_step_3.dart | 40 +---- lib/pages/shopinbit/shopinbit_step_4.dart | 128 +++++--------- .../desktop_gift_cards_view.dart | 0 .../services/desktop_services_view.dart | 4 +- .../desktop_shopinbit_view.dart | 66 ++------ .../desktop_shopin_bit_first_run.dart | 65 +++++++ lib/route_generator.dart | 4 +- .../nested_navigator_dialog.dart | 76 +++++++++ ...sted_navigator_dialog_route_generator.dart | 160 ++++++++++++++++++ 11 files changed, 395 insertions(+), 266 deletions(-) rename lib/pages_desktop_specific/services/{sub_widgets => cakepay}/desktop_gift_cards_view.dart (100%) rename lib/pages_desktop_specific/services/{sub_widgets => shopin_bit}/desktop_shopinbit_view.dart (88%) create mode 100644 lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart create mode 100644 lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart create mode 100644 lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart diff --git a/lib/pages/shopinbit/shopinbit_step_1.dart b/lib/pages/shopinbit/shopinbit_step_1.dart index 9a975aeb1..a1fa23694 100644 --- a/lib/pages/shopinbit/shopinbit_step_1.dart +++ b/lib/pages/shopinbit/shopinbit_step_1.dart @@ -32,18 +32,9 @@ class _ShopInBitStep1State extends State { void _continue() { widget.model.displayName = _nameController.text.trim(); - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep2(model: widget.model), - ); - } else { - Navigator.of( - context, - ).pushNamed(ShopInBitStep2.routeName, arguments: widget.model); - } + Navigator.of( + context, + ).pushNamed(ShopInBitStep2.routeName, arguments: widget.model); } @override diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart index 79d6a20ec..b69cc8797 100644 --- a/lib/pages/shopinbit/shopinbit_step_2.dart +++ b/lib/pages/shopinbit/shopinbit_step_2.dart @@ -15,7 +15,6 @@ 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'; import 'shopinbit_step_4.dart'; @@ -33,46 +32,19 @@ class ShopInBitStep2 extends StatefulWidget { class _ShopInBitStep2State extends State { ShopInBitCategory? _selected; - void _popBack() { - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep1(model: widget.model), - ); - } else { - Navigator.of(context).pop(); - } - } - void _continue() { widget.model.category = _selected; final skipGuidelines = ShopInBitService.instance.loadGuidelinesAccepted(); - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - if (skipGuidelines) { - widget.model.guidelinesAccepted = true; - Navigator.of( - context, - ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); - } else { - Navigator.of( - context, - ).pushNamed(ShopInBitStep3.routeName, arguments: widget.model); - } + if (skipGuidelines) { + widget.model.guidelinesAccepted = true; + Navigator.of( + context, + ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); } else { - if (skipGuidelines) { - widget.model.guidelinesAccepted = true; - Navigator.of( - context, - ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); - } else { - Navigator.of( - context, - ).pushNamed(ShopInBitStep3.routeName, arguments: widget.model); - } + Navigator.of( + context, + ).pushNamed(ShopInBitStep3.routeName, arguments: widget.model); } } @@ -101,11 +73,7 @@ class _ShopInBitStep2State extends State { children: [ Row( children: [ - AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: _popBack, - ), + const AppBarBackButton(isCompact: true, iconSize: 23), Text("ShopinBit", style: STextStyles.desktopH3(context)), ], ), @@ -125,40 +93,29 @@ class _ShopInBitStep2State extends State { 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()!.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), + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + 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_3.dart b/lib/pages/shopinbit/shopinbit_step_3.dart index 21f7b146f..4f6b799c4 100644 --- a/lib/pages/shopinbit/shopinbit_step_3.dart +++ b/lib/pages/shopinbit/shopinbit_step_3.dart @@ -12,7 +12,6 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/rounded_white_container.dart'; import '../exchange_view/sub_widgets/step_row.dart'; -import 'shopinbit_step_2.dart'; import 'shopinbit_step_4.dart'; class ShopInBitStep3 extends StatefulWidget { @@ -74,35 +73,14 @@ class _ShopInBitStep3State extends State { } } - void _popBack() { - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep2(model: widget.model), - ); - } else { - Navigator.of(context).pop(); - } - } - void _continue() { widget.model.guidelinesAccepted = true; // Persist acceptance. ShopInBitService.instance.setGuidelinesAccepted(true); - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep4(model: widget.model), - ); - } else { - Navigator.of( - context, - ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); - } + + Navigator.of( + context, + ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); } @override @@ -184,11 +162,7 @@ class _ShopInBitStep3State extends State { children: [ Row( children: [ - AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: _popBack, - ), + const AppBarBackButton(isCompact: true, iconSize: 23), Text("ShopinBit", style: STextStyles.desktopH3(context)), ], ), @@ -213,9 +187,7 @@ class _ShopInBitStep3State extends State { child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - leading: AppBarBackButton( - onPressed: () => Navigator.of(context).pop(), - ), + leading: const AppBarBackButton(), title: Text("ShopinBit", style: STextStyles.navBarTitle(context)), ), body: SafeArea( diff --git a/lib/pages/shopinbit/shopinbit_step_4.dart b/lib/pages/shopinbit/shopinbit_step_4.dart index 3ead68b6e..ee5c4114e 100644 --- a/lib/pages/shopinbit/shopinbit_step_4.dart +++ b/lib/pages/shopinbit/shopinbit_step_4.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -5,8 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'dart:async'; - import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; @@ -17,16 +17,15 @@ import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../widgets/background.dart'; -import '../../widgets/stack_dialog.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; import '../exchange_view/sub_widgets/step_row.dart'; -import 'shopinbit_step_3.dart'; import 'shopinbit_car_fee_view.dart'; import 'shopinbit_order_created.dart'; import 'shopinbit_tickets_view.dart'; @@ -130,7 +129,7 @@ class _ShopInBitStep4State extends State { final shouldContinue = await showDialog( context: context, barrierDismissible: false, - builder: (_) => Util.isDesktop + builder: (context) => Util.isDesktop ? DesktopDialog( maxWidth: 550, maxHeight: 250, @@ -157,24 +156,14 @@ class _ShopInBitStep4State extends State { width: 200, buttonHeight: ButtonHeight.l, label: "Cancel", - onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); - }, + onPressed: () => Navigator.of(context).pop(false), ), const SizedBox(width: 20), PrimaryButton( width: 200, buttonHeight: ButtonHeight.l, label: "Continue", - onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); - }, + onPressed: () => Navigator.of(context).pop(true), ), ], ), @@ -188,27 +177,13 @@ class _ShopInBitStep4State extends State { "You are about to open " "${uri.scheme}://${uri.host} " "in your browser.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text("Continue", style: STextStyles.button(context)), + rightButton: PrimaryButton( + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), ), ), ); @@ -445,19 +420,6 @@ class _ShopInBitStep4State extends State { super.dispose(); } - void _popBack() { - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep3(model: widget.model), - ); - } else { - Navigator.of(context).pop(); - } - } - Future _fetchCountries() async { setState(() => _loadingCountries = true); try { @@ -624,7 +586,8 @@ class _ShopInBitStep4State extends State { assert( widget.model.category != null, - 'Step 4 reached with null category: Step 2 must set category before reaching Step 4', + 'Step 4 reached with null category: Step 2 must set category before' + ' reaching Step 4', ); // API service_type: travel requests use "concierge" because the @@ -1063,7 +1026,8 @@ class _ShopInBitStep4State extends State { : STextStyles.field(context), decoration: standardInputDecoration( - "Describe what you'd like to purchase (e.g., electronics, luxury goods, services...)", + "Describe what you'd like to purchase " + "(e.g., electronics, luxury goods, services...)", _whatToPurchaseFocusNode, context, desktopMed: isDesktop, @@ -1555,7 +1519,8 @@ class _ShopInBitStep4State extends State { ), const TextSpan( text: - "\u20AC223 (incl. VAT): one-time payment, credited toward your purchase.", + "\u20AC223 (incl. VAT): one-time payment, " + "credited toward your purchase.", ), ], ), @@ -1968,7 +1933,8 @@ class _ShopInBitStep4State extends State { : STextStyles.field(context), decoration: standardInputDecoration( - "Describe your specific requirements (luggage, cabin class, hotel stars, etc.)", + "Describe your specific requirements " + "(luggage, cabin class, hotel stars, etc.)", _arrangementDetailsFocusNode, context, desktopMed: isDesktop, @@ -2407,11 +2373,7 @@ class _ShopInBitStep4State extends State { children: [ Row( children: [ - AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: _popBack, - ), + const AppBarBackButton(isCompact: true, iconSize: 23), Text("ShopinBit", style: STextStyles.desktopH3(context)), ], ), @@ -2433,37 +2395,27 @@ class _ShopInBitStep4State extends State { } return Background( - child: PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, dynamic result) { - if (!didPop) { - _popBack(); - } - }, - child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.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), + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + 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_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart b/lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart similarity index 100% rename from lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart rename to lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart diff --git a/lib/pages_desktop_specific/services/desktop_services_view.dart b/lib/pages_desktop_specific/services/desktop_services_view.dart index 26b6e9e59..f94f70883 100644 --- a/lib/pages_desktop_specific/services/desktop_services_view.dart +++ b/lib/pages_desktop_specific/services/desktop_services_view.dart @@ -9,8 +9,8 @@ import '../../utilities/text_styles.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; import '../../widgets/desktop/desktop_scaffold.dart'; import '../settings/settings_menu_item.dart'; -import 'sub_widgets/desktop_gift_cards_view.dart'; -import 'sub_widgets/desktop_shopinbit_view.dart'; +import 'cakepay/desktop_gift_cards_view.dart'; +import 'shopin_bit/desktop_shopinbit_view.dart'; final selectedServicesMenuItemStateProvider = StateProvider((_) => 0); diff --git a/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart similarity index 88% rename from lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart rename to lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index 010adc5a0..956a77cd7 100644 --- a/lib/pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -20,11 +20,13 @@ 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/nested_navigator_dialog/nested_navigator_dialog.dart'; import '../../../widgets/rounded_container.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/textfields/adaptive_text_field.dart'; import '../../desktop_menu.dart'; import '../../settings/settings_menu.dart'; +import 'sub_widgets/desktop_shopin_bit_first_run.dart'; class DesktopShopInBitView extends ConsumerStatefulWidget { const DesktopShopInBitView({super.key}); @@ -118,60 +120,9 @@ class _DesktopServicesViewState extends ConsumerState { await showDialog( context: context, barrierDismissible: false, - builder: (dialogContext) => DesktopDialog( - maxWidth: 550, - maxHeight: 300, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("ShopinBit", style: STextStyles.desktopH2(dialogContext)), - const SizedBox(height: 16), - RichText( - text: TextSpan( - style: STextStyles.desktopTextSmall(dialogContext), - children: const [ - TextSpan( - text: - "Please note the following before proceeding:" - "\n\n\u2022 Minimum order amount: 1,000 EUR" - "\n\u2022 Service fee: 10% of the order total", - ), - ], - ), - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of(dialogContext, rootNavigator: true).pop(); - }, - ), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () async { - Navigator.of(dialogContext, rootNavigator: true).pop(); - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => ShopInBitStep1(model: model), - ); - if (mounted) setState(() {}); - }, - ), - ], - ), - ], - ), - ), + builder: (_) => NestedNavigatorDialog( + initialRoute: DesktopShopinBitFirstRun.routeName, + initialRouteArgs: model, ), ); } else { @@ -179,8 +130,13 @@ class _DesktopServicesViewState extends ConsumerState { await showDialog( context: context, barrierDismissible: false, - builder: (_) => ShopInBitStep1(model: model), + builder: (_) => NestedNavigatorDialog( + initialRoute: ShopInBitStep1.routeName, + initialRouteArgs: model, + ), ); + + // TODO: figure out and comment why this is needed if (mounted) setState(() {}); } } diff --git a/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart new file mode 100644 index 000000000..69b6dfffb --- /dev/null +++ b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/shopinbit/shopinbit_order_model.dart'; +import '../../../../pages/shopinbit/shopinbit_step_1.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/dialogs/s_dialog.dart'; + +class DesktopShopinBitFirstRun extends StatelessWidget { + const DesktopShopinBitFirstRun({super.key, required this.model}); + + static const routeName = "/desktopShopinBitFirstRun"; + + final ShopInBitOrderModel model; + + @override + Widget build(BuildContext context) { + return SDialog( + child: SizedBox( + width: 580, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("ShopinBit", style: STextStyles.desktopH2(context)), + const SizedBox(height: 16), + RichText( + text: TextSpan( + style: STextStyles.desktopTextSmall(context), + children: const [ + TextSpan( + text: + "Please note the following before proceeding:" + "\n\n\u2022 Minimum order amount: 1,000 EUR" + "\n\u2022 Service fee: 10% of the order total", + ), + ], + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SecondaryButton( + width: 220, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + PrimaryButton( + width: 220, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () => Navigator.of( + context, + ).pushNamed(ShopInBitStep1.routeName, arguments: model), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 0f0fbbf58..d08cdb165 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -241,9 +241,9 @@ import 'pages_desktop_specific/password/create_password_view.dart'; import 'pages_desktop_specific/password/delete_password_warning_view.dart'; import 'pages_desktop_specific/password/forgot_password_desktop_view.dart'; import 'pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart'; +import 'pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart'; import 'pages_desktop_specific/services/desktop_services_view.dart'; -import 'pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart'; -import 'pages_desktop_specific/services/sub_widgets/desktop_shopinbit_view.dart'; +import 'pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart'; import 'pages_desktop_specific/settings/desktop_settings_view.dart'; import 'pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart'; import 'pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart'; diff --git a/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart new file mode 100644 index 000000000..ae54f23f2 --- /dev/null +++ b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import 'nested_navigator_dialog_route_generator.dart'; + +class NestedNavigatorDialog extends StatefulWidget { + const NestedNavigatorDialog({ + super.key, + required this.initialRoute, + this.initialRouteArgs, + this.navigatorKey, + }); + + final String initialRoute; + final Object? initialRouteArgs; + final GlobalKey? navigatorKey; + + @override + State createState() => _NestedNavigatorDialogState(); +} + +class _NestedNavigatorDialogState extends State { + late final _CloseOnEmptyObserver _observer; + late final GlobalKey _navigatorKey; + + NavigatorState? _parentNavigator; + + void _close() { + if (mounted) _parentNavigator?.pop(); + } + + @override + void initState() { + super.initState(); + _observer = _CloseOnEmptyObserver(_close); + _navigatorKey = widget.navigatorKey ?? GlobalKey(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _parentNavigator = Navigator.of(context); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + insetPadding: EdgeInsets.zero, + child: Navigator( + key: _navigatorKey, + observers: [_observer], + onGenerateRoute: NestedNavigatorDialogRouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, _) => [ + NestedNavigatorDialogRouteGenerator.generateRoute( + RouteSettings( + name: widget.initialRoute, + arguments: widget.initialRouteArgs, + ), + ), + ], + ), + ); + } +} + +class _CloseOnEmptyObserver extends NavigatorObserver { + _CloseOnEmptyObserver(this.onEmpty); + + final VoidCallback onEmpty; + + @override + void didPop(Route route, Route? previousRoute) { + if (previousRoute == null) onEmpty(); + } +} 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 new file mode 100644 index 000000000..7d924861b --- /dev/null +++ b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog_route_generator.dart @@ -0,0 +1,160 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../../models/shopinbit/shopinbit_order_model.dart'; +import '../../../pages/shopinbit/shopinbit_step_1.dart'; +import '../../../pages/shopinbit/shopinbit_step_2.dart'; +import '../../../pages/shopinbit/shopinbit_step_3.dart'; +import '../../../pages/shopinbit/shopinbit_step_4.dart'; +import '../../../pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../conditional_parent.dart'; +import '../../desktop/desktop_dialog_close_button.dart'; +import '../s_dialog.dart'; + +abstract final class NestedNavigatorDialogRouteGenerator { + static Route generateRoute(RouteSettings settings) { + final args = settings.arguments; + + switch (settings.name) { + case DesktopShopinBitFirstRun.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => DesktopShopinBitFirstRun(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + + case ShopInBitStep1.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => ShopInBitStep1(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + + case ShopInBitStep2.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => ShopInBitStep2(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + + case ShopInBitStep3.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => ShopInBitStep3(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + + case ShopInBitStep4.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => ShopInBitStep4(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + + default: + return _routeError("Unknown route name: ${settings.name}"); + } + } + + static Route getRoute({ + required WidgetBuilder builder, + RouteSettings? settings, + }) { + return PageRouteBuilder( + settings: settings, + opaque: false, + barrierColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 220), + reverseTransitionDuration: const Duration(milliseconds: 220), + pageBuilder: (BuildContext context, _, __) => builder(context), + transitionsBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: animation, + child: FadeTransition( + opacity: Tween( + begin: 1, + end: 0, + ).animate(secondaryAnimation), + child: child, + ), + ); + }, + ); + } + + static Route _routeError(String message) { + return getRoute( + builder: (context) => SDialog( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox( + width: 580, + child: Column( + mainAxisSize: .min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Navigation Error", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + child, + const SizedBox(height: 32), + ], + ), + ), + child: Text( + "Error handling route, this is not supposed to happen. " + "Contact developers.\n$message", + ), + ), + ), + ); + } +} From e9beb4ab2b6609b19c853f66da04e826b5f3eb6d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 May 2026 18:39:25 -0600 Subject: [PATCH 09/27] refactor: testing AI refactoring --- lib/pages/shopinbit/shopinbit_step_4.dart | 2427 +---------------- .../shopinbit_car_research_form.dart | 347 +++ .../shopinbit_concierge_form.dart | 191 ++ .../shopinbit_country_picker.dart | 164 ++ .../shopinbit_generic_form.dart | 112 + .../shopinbit_labeled_checkbox.dart | 49 + .../shopinbit_privacy_checkbox.dart | 163 ++ .../shopinbit_step4_dropdown.dart | 93 + .../shopinbit_step4_header.dart | 46 + .../shopinbit_step4_submit.dart | 96 + .../shopinbit_step4_submit_button.dart | 25 + .../shopinbit_step4_text_field.dart | 89 + .../shopinbit_travel_form.dart | 544 ++++ .../shopinbit_traveler_counter.dart | 85 + 14 files changed, 2062 insertions(+), 2369 deletions(-) create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_labeled_checkbox.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_step4_dropdown.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_step4_header.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_step4_submit_button.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_step4_text_field.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart create mode 100644 lib/pages/shopinbit/step_4_components/shopinbit_traveler_counter.dart diff --git a/lib/pages/shopinbit/shopinbit_step_4.dart b/lib/pages/shopinbit/shopinbit_step_4.dart index ee5c4114e..c5cfd4fff 100644 --- a/lib/pages/shopinbit/shopinbit_step_4.dart +++ b/lib/pages/shopinbit/shopinbit_step_4.dart @@ -1,36 +1,20 @@ -import 'dart:async'; - -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../db/isar/main_db.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/assets.dart'; -import '../../utilities/constants.dart'; -import '../../utilities/text_styles.dart'; -import '../../utilities/util.dart'; -import '../../widgets/background.dart'; -import '../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../widgets/desktop/desktop_dialog.dart'; -import '../../widgets/desktop/desktop_dialog_close_button.dart'; -import '../../widgets/desktop/primary_button.dart'; -import '../../widgets/desktop/secondary_button.dart'; -import '../../widgets/rounded_white_container.dart'; -import '../../widgets/stack_dialog.dart'; -import '../../widgets/stack_text_field.dart'; -import '../exchange_view/sub_widgets/step_row.dart'; -import 'shopinbit_car_fee_view.dart'; -import 'shopinbit_order_created.dart'; -import 'shopinbit_tickets_view.dart'; - -class ShopInBitStep4 extends StatefulWidget { +import "package:flutter/material.dart"; + +import "../../models/shopinbit/shopinbit_order_model.dart"; +import "../../themes/stack_colors.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 "step_4_components/shopinbit_car_research_form.dart"; +import "step_4_components/shopinbit_concierge_form.dart"; +import "step_4_components/shopinbit_generic_form.dart"; +import "step_4_components/shopinbit_travel_form.dart"; + +class ShopInBitStep4 extends StatelessWidget { const ShopInBitStep4({super.key, required this.model}); static const String routeName = "/shopInBitStep4"; @@ -38,2362 +22,67 @@ class ShopInBitStep4 extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitStep4State(); -} - -class _ShopInBitStep4State extends State { - // Generic form controllers. - late final TextEditingController _descriptionController; - late final FocusNode _descriptionFocusNode; - final TextEditingController _countrySearchController = - TextEditingController(); - - // Concierge-specific controllers - late final TextEditingController _whatToPurchaseController; - late final FocusNode _whatToPurchaseFocusNode; - late final TextEditingController _budgetController; - late final FocusNode _budgetFocusNode; - String? _selectedCondition; - bool _noLimit = false; - bool _whatToPurchaseTouched = false; - bool _budgetTouched = false; - - // Car Research-specific controllers - late final TextEditingController _brandController; - late final FocusNode _brandFocusNode; - late final TextEditingController _modelController; - late final FocusNode _modelFocusNode; - late final TextEditingController _carDescriptionController; - late final FocusNode _carDescriptionFocusNode; - late final TextEditingController _carBudgetController; - late final FocusNode _carBudgetFocusNode; - String? _selectedCarCondition; - bool _feeAcknowledged = false; - bool _brandTouched = false; - bool _modelTouched = false; - bool _carDescriptionTouched = false; - bool _carBudgetTouched = false; - - // Travel-specific controllers - late final TextEditingController _departureCountryController; - late final FocusNode _departureCountryFocusNode; - String? _selectedDepartureCountryIso; - final TextEditingController _departureCountrySearchController = - TextEditingController(); - late final TextEditingController _arrangementDetailsController; - late final FocusNode _arrangementDetailsFocusNode; - bool _arrangementDetailsTouched = false; - late final TextEditingController _departureCityController; - late final FocusNode _departureCityFocusNode; - late final TextEditingController _destinationsController; - late final FocusNode _destinationsFocusNode; - late final TextEditingController _departureDateController; - late final FocusNode _departureDateFocusNode; - late final TextEditingController _returnDateController; - late final FocusNode _returnDateFocusNode; - late final TextEditingController _tripLengthController; - late final FocusNode _tripLengthFocusNode; - late final TextEditingController _travelBudgetController; - late final FocusNode _travelBudgetFocusNode; - - // Travel dropdown state - String? _selectedArrangement; - String? _selectedDateMode; - String? _selectedFlexibility; - String? _selectedYear; - String? _selectedMonthSeason; - bool _needsRecommendations = false; - int _adults = 1; - int _children = 0; - int _infants = 0; - int _pets = 0; - - // Travel touched booleans - bool _departureCountryTouched = false; - bool _departureCityTouched = false; - bool _destinationsTouched = false; - bool _departureDateTouched = false; - bool _returnDateTouched = false; - bool _tripLengthTouched = false; - bool _travelBudgetTouched = false; - - List> _countries = []; - String? _selectedCountryIso; - bool _loadingCountries = false; - - bool _submitting = false; - bool _privacyAccepted = false; - - Future _showOpenBrowserWarning(BuildContext context, String url) async { - final uri = Uri.parse(url); - final shouldContinue = await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Util.isDesktop - ? DesktopDialog( - maxWidth: 550, - maxHeight: 250, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 20, - ), - child: Column( - children: [ - Text("Attention", style: STextStyles.desktopH2(context)), - const SizedBox(height: 16), - Text( - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 35), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () => Navigator.of(context).pop(false), - ), - const SizedBox(width: 20), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () => Navigator.of(context).pop(true), - ), - ], - ), - ], - ), - ), - ) - : StackDialog( - title: "Attention", - message: - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - leftButton: SecondaryButton( - label: "Cancel", - onPressed: () => Navigator.of(context).pop(false), - ), - rightButton: PrimaryButton( - label: "Continue", - onPressed: () => Navigator.of(context).pop(true), - ), - ), - ); - return shouldContinue ?? false; - } - - bool get _budgetIsValid { - final text = _budgetController.text.trim(); - if (text.isEmpty) return false; - final value = int.tryParse(text); - return value != null && value >= 1000 && value <= 100000; - } - - bool get _canContinue { - final cat = widget.model.category; - if (cat == ShopInBitCategory.concierge) { - return !_submitting && - _privacyAccepted && - _whatToPurchaseController.text.trim().length >= 10 && - _selectedCondition != null && - (_noLimit || _budgetIsValid) && - _selectedCountryIso != null; - } - if (cat == ShopInBitCategory.car) { - final carBudgetVal = int.tryParse(_carBudgetController.text.trim()); - return !_submitting && - _privacyAccepted && - _feeAcknowledged && - _brandController.text.trim().length >= 3 && - _modelController.text.trim().length >= 3 && - _carDescriptionController.text.trim().length >= 3 && - _selectedCarCondition != null && - carBudgetVal != null && - carBudgetVal >= 20000 && - _selectedCountryIso != null; - } - if (cat == ShopInBitCategory.travel) { - final travelBudgetVal = int.tryParse(_travelBudgetController.text.trim()); - final hasValidDates = _selectedDateMode == "Flexible dates" - ? (_selectedYear != null && - _selectedMonthSeason != null && - _tripLengthController.text.trim().isNotEmpty) - : (_selectedDateMode == "Exact dates" && - _departureDateController.text.trim().isNotEmpty && - _returnDateController.text.trim().isNotEmpty); - return !_submitting && - _privacyAccepted && - _selectedArrangement != null && - _arrangementDetailsController.text.trim().length >= 10 && - _selectedDepartureCountryIso != null && - _departureCityController.text.trim().isNotEmpty && - (_needsRecommendations || - _destinationsController.text.trim().isNotEmpty) && - _selectedDateMode != null && - hasValidDates && - _adults >= 1 && - travelBudgetVal != null && - travelBudgetVal >= 1000; - } - // generic fallback - return !_submitting && - _privacyAccepted && - _descriptionController.text.trim().isNotEmpty && - _selectedCountryIso != null; - } - - @override - void initState() { - super.initState(); - _descriptionController = TextEditingController( - text: widget.model.requestDescription, - ); - _descriptionFocusNode = FocusNode(); - _descriptionFocusNode.addListener(() => setState(() {})); - - // Concierge-specific init - _whatToPurchaseController = TextEditingController(); - _whatToPurchaseFocusNode = FocusNode(); - _whatToPurchaseFocusNode.addListener(() { - if (!_whatToPurchaseFocusNode.hasFocus) { - _whatToPurchaseTouched = true; - } - setState(() {}); - }); - _budgetController = TextEditingController(text: "1000"); - _budgetFocusNode = FocusNode(); - _budgetFocusNode.addListener(() { - if (!_budgetFocusNode.hasFocus) { - _budgetTouched = true; - } - setState(() {}); - }); - - // Car Research-specific init - _brandController = TextEditingController(); - _brandFocusNode = FocusNode(); - _brandFocusNode.addListener(() { - if (!_brandFocusNode.hasFocus) { - _brandTouched = true; - } - setState(() {}); - }); - _modelController = TextEditingController(); - _modelFocusNode = FocusNode(); - _modelFocusNode.addListener(() { - if (!_modelFocusNode.hasFocus) { - _modelTouched = true; - } - setState(() {}); - }); - _carDescriptionController = TextEditingController(); - _carDescriptionFocusNode = FocusNode(); - _carDescriptionFocusNode.addListener(() { - if (!_carDescriptionFocusNode.hasFocus) { - _carDescriptionTouched = true; - } - setState(() {}); - }); - _carBudgetController = TextEditingController(); - _carBudgetFocusNode = FocusNode(); - _carBudgetFocusNode.addListener(() { - if (!_carBudgetFocusNode.hasFocus) { - _carBudgetTouched = true; - } - setState(() {}); - }); - - // Travel-specific init - _departureCountryController = TextEditingController(); - _departureCountryFocusNode = FocusNode(); - _departureCountryFocusNode.addListener(() { - if (!_departureCountryFocusNode.hasFocus) { - _departureCountryTouched = true; - } - setState(() {}); - }); - _arrangementDetailsController = TextEditingController(); - _arrangementDetailsFocusNode = FocusNode(); - _arrangementDetailsFocusNode.addListener(() { - if (!_arrangementDetailsFocusNode.hasFocus) { - _arrangementDetailsTouched = true; - } - setState(() {}); - }); - _departureCityController = TextEditingController(); - _departureCityFocusNode = FocusNode(); - _departureCityFocusNode.addListener(() { - if (!_departureCityFocusNode.hasFocus) { - _departureCityTouched = true; - } - setState(() {}); - }); - _destinationsController = TextEditingController(); - _destinationsFocusNode = FocusNode(); - _destinationsFocusNode.addListener(() { - if (!_destinationsFocusNode.hasFocus) { - _destinationsTouched = true; - } - setState(() {}); - }); - _departureDateController = TextEditingController(); - _departureDateFocusNode = FocusNode(); - _departureDateFocusNode.addListener(() { - if (!_departureDateFocusNode.hasFocus) { - _departureDateTouched = true; - } - setState(() {}); - }); - _returnDateController = TextEditingController(); - _returnDateFocusNode = FocusNode(); - _returnDateFocusNode.addListener(() { - if (!_returnDateFocusNode.hasFocus) { - _returnDateTouched = true; - } - setState(() {}); - }); - _tripLengthController = TextEditingController(); - _tripLengthFocusNode = FocusNode(); - _tripLengthFocusNode.addListener(() { - if (!_tripLengthFocusNode.hasFocus) { - _tripLengthTouched = true; - } - setState(() {}); - }); - _travelBudgetController = TextEditingController(text: "5000"); - _travelBudgetFocusNode = FocusNode(); - _travelBudgetFocusNode.addListener(() { - if (!_travelBudgetFocusNode.hasFocus) { - _travelBudgetTouched = true; - } - setState(() {}); - }); - - if (widget.model.deliveryCountry.isNotEmpty) { - _selectedCountryIso = widget.model.deliveryCountry; - } - _fetchCountries(); - } - - @override - void dispose() { - _descriptionController.dispose(); - _descriptionFocusNode.dispose(); - _countrySearchController.dispose(); - _whatToPurchaseController.dispose(); - _whatToPurchaseFocusNode.dispose(); - _budgetController.dispose(); - _budgetFocusNode.dispose(); - _brandController.dispose(); - _brandFocusNode.dispose(); - _modelController.dispose(); - _modelFocusNode.dispose(); - _carDescriptionController.dispose(); - _carDescriptionFocusNode.dispose(); - _carBudgetController.dispose(); - _carBudgetFocusNode.dispose(); - _departureCountryController.dispose(); - _departureCountryFocusNode.dispose(); - _departureCountrySearchController.dispose(); - _arrangementDetailsController.dispose(); - _arrangementDetailsFocusNode.dispose(); - _departureCityController.dispose(); - _departureCityFocusNode.dispose(); - _destinationsController.dispose(); - _destinationsFocusNode.dispose(); - _departureDateController.dispose(); - _departureDateFocusNode.dispose(); - _returnDateController.dispose(); - _returnDateFocusNode.dispose(); - _tripLengthController.dispose(); - _tripLengthFocusNode.dispose(); - _travelBudgetController.dispose(); - _travelBudgetFocusNode.dispose(); - super.dispose(); - } - - Future _fetchCountries() async { - setState(() => _loadingCountries = true); - try { - final resp = await ShopInBitService.instance.client.getCountries(); - if (resp.hasError || resp.value == null) return; - _countries = resp.value!; - if (_selectedCountryIso != null && - !_countries.any((c) => c['iso'] == _selectedCountryIso)) { - _selectedCountryIso = null; - } - } catch (_) { - // leave list empty; user will see no items - } finally { - if (mounted) setState(() => _loadingCountries = false); - } - } - - Future _submit() async { - // Format structured comment per category. - // Use ISO code for delivery country in comment: country labels can - // contain non-ASCII (e.g. "Åland Islands") which HttpClientRequest.write() - // encodes as Latin-1, corrupting the JSON body on mobile. - final countryIso = _selectedCountryIso!; - if (widget.model.category == ShopInBitCategory.concierge) { - final budgetText = _noLimit - ? "No limit" - : "${_budgetController.text.trim()} EUR"; - widget.model.requestDescription = - "What to purchase: ${_whatToPurchaseController.text.trim()}\n" - "Condition: $_selectedCondition\n" - "Budget: $budgetText\n" - "Delivery country: $countryIso"; - } else if (widget.model.category == ShopInBitCategory.car) { - widget.model.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"; - } else if (widget.model.category == ShopInBitCategory.travel) { - final parts = [ - "Arrangement: $_selectedArrangement", - "Details: ${_arrangementDetailsController.text.trim()}", - "Departure: ${_departureCityController.text.trim()}, " - "${_selectedDepartureCountryIso ?? ''}", - ]; - - if (_needsRecommendations) { - parts.add("Destinations: Recommendations requested"); - } else { - parts.add("Destinations: ${_destinationsController.text.trim()}"); - } - - if (_selectedDateMode == "Exact dates") { - final flex = - _selectedFlexibility != null && _selectedFlexibility != "Exact" - ? " ($_selectedFlexibility)" - : ""; - parts.add( - "Dates: ${_departureDateController.text.trim()} - " - "${_returnDateController.text.trim()}$flex", - ); - } else if (_selectedDateMode == "Flexible dates") { - parts.add( - "Dates: $_selectedMonthSeason $_selectedYear, " - "${_tripLengthController.text.trim()} nights", - ); - } - - final travelers = []; - travelers.add("$_adults adult${_adults > 1 ? 's' : ''}"); - if (_children > 0) { - travelers.add("$_children child${_children > 1 ? 'ren' : ''}"); - } - if (_infants > 0) { - travelers.add("$_infants infant${_infants > 1 ? 's' : ''}"); - } - if (_pets > 0) { - travelers.add("$_pets pet${_pets > 1 ? 's' : ''}"); - } - parts.add("Travelers: ${travelers.join(', ')}"); - - parts.add("Budget: ${_travelBudgetController.text.trim()} EUR"); - - widget.model.requestDescription = parts.join("\n"); - } else { - widget.model.requestDescription = _descriptionController.text.trim(); - } - // Travel doesn't collect delivery country: use departure country or "DE" - // as a default since the API requires the field. - if (widget.model.category == ShopInBitCategory.travel) { - widget.model.deliveryCountry = "DE"; - } else { - widget.model.deliveryCountry = _selectedCountryIso!; - } - - if (widget.model.category == ShopInBitCategory.car) { - // Block if another car research flow is already in progress. - final existingPending = MainDB.instance - .getShopInBitTickets() - .where((t) => t.isPendingPayment) - .toList(); - - if (existingPending.isNotEmpty && mounted) { - final resumePrevious = await showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => AlertDialog( - title: const Text("In-Progress Car Research"), - content: const Text( - "You have an unfinished car research payment. " - "Would you like to resume it or start a new search?", - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(true), - child: const Text("Resume Previous"), - ), - TextButton( - onPressed: () => Navigator.of(ctx).pop(false), - child: const Text("Start New"), - ), - ], - ), - ); - - if (resumePrevious == true && mounted) { - setState(() => _submitting = false); - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - ShopInBitTicketsView.routeName, - (route) => route.isFirst, - ), - ); - return; - } - } - - if (!mounted) return; - - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showDialog( - context: context, - builder: (_) => ShopInBitCarFeeView(model: widget.model), - ), - ); - } else { - unawaited( - Navigator.of( - context, - ).pushNamed(ShopInBitCarFeeView.routeName, arguments: widget.model), - ); - } - return; - } - - setState(() => _submitting = true); - try { - final service = ShopInBitService.instance; - final customerKey = await service.ensureCustomerKey(); - - assert( - widget.model.category != null, - 'Step 4 reached with null category: Step 2 must set category before' - ' reaching Step 4', - ); - - // API service_type: travel requests use "concierge" because the - // ShopinBit API routes both through the same concierge pipeline. - // Travel-specific details are captured in the structured comment field. - final categoryStr = switch (widget.model.category) { - ShopInBitCategory.concierge => "concierge", - ShopInBitCategory.travel => "concierge", - ShopInBitCategory.car => "car", - null => throw StateError('category must be non-null at Step 4 submit'), - }; - - final resp = await service.client.createRequest( - customerPseudonym: widget.model.displayName, - externalCustomerKey: customerKey, - serviceType: categoryStr, - comment: widget.model.requestDescription, - deliveryCountry: widget.model.deliveryCountry, - ); - - if (resp.hasError) { - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: resp.exception?.message ?? "Failed to create request", - context: context, - ), - ); - } - return; - } - - final ref = resp.value!; - widget.model.apiTicketId = ref.id; - widget.model.ticketId = ref.number; - widget.model.status = ShopInBitOrderStatus.pending; - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); - - if (!mounted) return; - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showDialog( - context: context, - builder: (_) => ShopInBitOrderCreated(model: widget.model), - ), - ); - } else { - unawaited( - Navigator.of( - context, - ).pushNamed(ShopInBitOrderCreated.routeName, arguments: widget.model), - ); - } - } catch (e) { - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to create request: $e", - context: context, - ), - ); - } - } finally { - if (mounted) setState(() => _submitting = false); - } - } - - // Shared widgets. - Widget _buildCountryPicker(bool isDesktop) { - return ClipRRect( - borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), - child: DropdownButtonHideUnderline( - child: DropdownButton2( - value: _selectedCountryIso, - items: _countries - .map( - (c) => DropdownMenuItem( - value: c['iso'] as String, - child: Text( - c['label'] as String, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ) - : STextStyles.w500_14(context), - ), - ), - ) - .toList(), - onMenuStateChange: (isOpen) { - if (!isOpen) { - _countrySearchController.clear(); - } - }, - onChanged: _loadingCountries - ? null - : (value) { - setState(() { - _selectedCountryIso = value; - }); - }, - hint: Text( - _loadingCountries ? "Loading countries..." : "Delivery country", - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldDefaultSearchIconLeft, - ) - : STextStyles.fieldLabel(context), - ), - isExpanded: true, - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveSearchIconRight, - ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, 0), - elevation: 0, - maxHeight: 300, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - dropdownSearchData: DropdownSearchData( - searchController: _countrySearchController, - searchInnerWidgetHeight: 48, - searchInnerWidget: TextFormField( - controller: _countrySearchController, - 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), - ), - ), - ), - ); - } - - Widget _buildDepartureCountryPicker(bool isDesktop) { - return ClipRRect( - borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), - child: DropdownButtonHideUnderline( - child: DropdownButton2( - value: _selectedDepartureCountryIso, - items: _countries - .map( - (c) => DropdownMenuItem( - value: c['iso'] as String, - child: Text( - c['label'] as String, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ) - : STextStyles.w500_14(context), - ), - ), - ) - .toList(), - onMenuStateChange: (isOpen) { - if (!isOpen) { - _departureCountrySearchController.clear(); - } - }, - onChanged: _loadingCountries - ? null - : (value) { - setState(() { - _selectedDepartureCountryIso = value; - _departureCountryTouched = true; - }); - }, - hint: Text( - _loadingCountries ? "Loading countries..." : "Departure country", - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldDefaultSearchIconLeft, - ) - : STextStyles.fieldLabel(context), - ), - isExpanded: true, - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveSearchIconRight, - ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, 0), - elevation: 0, - maxHeight: 300, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - dropdownSearchData: DropdownSearchData( - searchController: _departureCountrySearchController, - searchInnerWidgetHeight: 48, - searchInnerWidget: TextFormField( - controller: _departureCountrySearchController, - 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), - ), - ), - ), - ); - } - - Widget _buildPrivacyCheckbox(bool isDesktop) { - return GestureDetector( - onTap: () { - setState(() { - _privacyAccepted = !_privacyAccepted; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(top: isDesktop ? 3 : 0), - child: SizedBox( - width: 20, - height: 20, - child: IgnorePointer( - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _privacyAccepted, - onChanged: (_) {}, - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: RichText( - text: TextSpan( - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - children: [ - const TextSpan( - text: "I have read and agree to the ShopinBit ", - ), - TextSpan( - text: "Privacy Policy", - style: STextStyles.richLink( - context, - ).copyWith(fontSize: isDesktop ? 18 : 14), - recognizer: TapGestureRecognizer() - ..onTap = () async { - const url = - "https://api.shopinbit.com/static/policy/privacy.html"; - final shouldOpen = await _showOpenBrowserWarning( - context, - url, - ); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } - }, - ), - const TextSpan(text: "."), - ], - ), - ), - ), - ], - ), + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => _ShopInBitStep4DesktopShell(content: child), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => _ShopInBitStep4MobileShell(content: child), + child: switch (model.category) { + ShopInBitCategory.concierge => ShopInBitConciergeForm(model: model), + ShopInBitCategory.car => ShopInBitCarResearchForm(model: model), + ShopInBitCategory.travel => ShopInBitTravelForm(model: model), + null => ShopInBitGenericForm(model: model), + }, ), ); } +} - Widget _buildSubmitButton() { - return PrimaryButton( - label: _submitting ? "Submitting..." : "Submit request", - enabled: _canContinue, - onPressed: _canContinue ? _submit : null, - ); - } - - // Per-category form builders. - - Widget _buildConciergeContent(bool isDesktop) { - final whatToPurchaseError = - _whatToPurchaseTouched && - _whatToPurchaseController.text.trim().length < 10 - ? "Minimum 10 characters" - : null; - - final budgetError = _budgetTouched && !_noLimit && !_budgetIsValid - ? "Enter a value between 1,000 and 100,000" - : null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isDesktop) - StepRow( - count: 4, - current: 3, - width: MediaQuery.of(context).size.width - 32, - ), - if (!isDesktop) const SizedBox(height: 14), - Text( - "What would you like to purchase?", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox(height: isDesktop ? 16 : 8), - Text( - "Tell us what you're looking for and we'll find it for you.", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - SizedBox(height: isDesktop ? 16 : 12), - - // What to purchase free-text field - TextField( - controller: _whatToPurchaseController, - focusNode: _whatToPurchaseFocusNode, - autocorrect: false, - enableSuggestions: false, - minLines: 3, - maxLines: 6, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Describe what you'd like to purchase " - "(e.g., electronics, luxury goods, services...)", - _whatToPurchaseFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: whatToPurchaseError, - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Condition picker - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton2( - value: _selectedCondition, - items: ["NEW", "USED"] - .map( - (c) => DropdownMenuItem( - value: c, - child: Text( - c, - style: isDesktop - ? STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ) - : STextStyles.w500_14(context), - ), - ), - ) - .toList(), - onChanged: (value) { - setState(() { - _selectedCondition = value; - }); - }, - hint: Text( - "Condition", - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, - ) - : STextStyles.fieldLabel(context), - ), - isExpanded: true, - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveSearchIconRight, - ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, 0), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Budget field - TextField( - controller: _budgetController, - focusNode: _budgetFocusNode, - autocorrect: false, - enableSuggestions: false, - enabled: !_noLimit, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Budget (\u20AC)", - _budgetFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixText: "\u20AC", - errorText: budgetError, - ), - ), - SizedBox(height: isDesktop ? 12 : 8), - - // No budget limit checkbox - GestureDetector( - onTap: () { - setState(() { - _noLimit = !_noLimit; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: IgnorePointer( - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _noLimit, - onChanged: (_) {}, - ), - ), - ), - const SizedBox(width: 12), - Text( - "No budget limit", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ], - ), - ), - ), - SizedBox(height: isDesktop ? 12 : 12), - - // Country picker (shared) - _buildCountryPicker(isDesktop), - SizedBox(height: isDesktop ? 12 : 12), - - // Privacy checkbox (shared) - _buildPrivacyCheckbox(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - - // Submit button (shared) - _buildSubmitButton(), - ], - ); - } - - Widget _buildCarContent(bool isDesktop) { - final brandError = _brandTouched && _brandController.text.trim().length < 3 - ? "Minimum 3 characters" - : null; - - final modelError = _modelTouched && _modelController.text.trim().length < 3 - ? "Minimum 3 characters" - : null; - - final carDescriptionError = - _carDescriptionTouched && - _carDescriptionController.text.trim().length < 3 - ? "Minimum 3 characters" - : null; - - final carBudgetText = _carBudgetController.text.trim(); - final carBudgetVal = int.tryParse(carBudgetText); - final carBudgetError = - _carBudgetTouched && - (carBudgetText.isEmpty || - carBudgetVal == null || - carBudgetVal < 20000) - ? "Minimum budget is 20,000\u20AC" - : null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isDesktop) - StepRow( - count: 4, - current: 3, - width: MediaQuery.of(context).size.width - 32, - ), - if (!isDesktop) const SizedBox(height: 14), - Text( - "Car Research request", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox(height: isDesktop ? 16 : 8), - Text( - "Tell us about the car you're looking for.", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - SizedBox(height: isDesktop ? 32 : 24), - - // Country picker (shared) - _buildCountryPicker(isDesktop), - SizedBox(height: isDesktop ? 24 : 16), - - // Brand field - TextField( - controller: _brandController, - focusNode: _brandFocusNode, - autocorrect: false, - enableSuggestions: false, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Car brand (e.g., BMW, Mercedes, Toyota...)", - _brandFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: brandError, - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Model field - TextField( - controller: _modelController, - focusNode: _modelFocusNode, - autocorrect: false, - enableSuggestions: false, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Car model (e.g., 3 Series, E-Class, Camry...)", - _modelFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: modelError, - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Condition picker - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton2( - value: _selectedCarCondition, - items: ["NEW", "PREOWNED"] - .map( - (c) => DropdownMenuItem( - value: c, - child: Text( - c, - style: isDesktop - ? STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ) - : STextStyles.w500_14(context), - ), - ), - ) - .toList(), - onChanged: (value) { - setState(() { - _selectedCarCondition = value; - }); - }, - hint: Text( - "Condition", - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, - ) - : STextStyles.fieldLabel(context), - ), - isExpanded: true, - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveSearchIconRight, - ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, 0), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Description field (multiline) - TextField( - controller: _carDescriptionController, - focusNode: _carDescriptionFocusNode, - autocorrect: false, - enableSuggestions: false, - minLines: 3, - maxLines: 6, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Describe your requirements (year, mileage, features...)", - _carDescriptionFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: carDescriptionError, - ), - ), - SizedBox(height: isDesktop ? 24 : 16), +class _ShopInBitStep4DesktopShell extends StatelessWidget { + const _ShopInBitStep4DesktopShell({required this.content}); - // Budget field - TextField( - controller: _carBudgetController, - focusNode: _carBudgetFocusNode, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Budget (\u20AC, minimum 20,000)", - _carBudgetFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixText: "\u20AC", - errorText: carBudgetError, - ), - ), - SizedBox(height: isDesktop ? 24 : 16), + final Widget content; - // Research fee info box - RoundedWhiteContainer( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 750, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon( - Icons.info_outline, - size: 20, - color: Theme.of( - context, - ).extension()!.textFieldActiveSearchIconLeft, - ), - const SizedBox(width: 12), - Expanded( - child: RichText( - text: TextSpan( - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - children: [ - TextSpan( - text: "Research fee: ", - style: isDesktop - ? STextStyles.desktopTextSmall( - context, - ).copyWith(fontWeight: FontWeight.bold) - : STextStyles.w500_14( - context, - ).copyWith(fontWeight: FontWeight.bold), - ), - const TextSpan( - text: - "\u20AC223 (incl. VAT): one-time payment, " - "credited toward your purchase.", - ), - ], - ), - ), + Row( + children: [ + const AppBarBackButton(isCompact: true, iconSize: 23), + Text("ShopinBit", style: STextStyles.desktopH3(context)), + ], ), + const DesktopDialogCloseButton(), ], ), - ), - SizedBox(height: isDesktop ? 16 : 12), - - // Fee acknowledgement checkbox - GestureDetector( - onTap: () { - setState(() { - _feeAcknowledged = !_feeAcknowledged; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: IgnorePointer( - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _feeAcknowledged, - onChanged: (_) {}, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - "I acknowledge the \u20AC223 research fee", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ), - ], + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: SingleChildScrollView(child: content), ), ), - ), - SizedBox(height: isDesktop ? 16 : 12), - - // Privacy checkbox (shared) - _buildPrivacyCheckbox(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - - // Submit button (shared) - _buildSubmitButton(), - ], - ); - } - - Widget _buildGenericContent(bool isDesktop) { - const descriptionTitle = "Describe your travel request"; - const descriptionSubtitle = "Provide details about your trip."; - const descriptionPlaceholder = - "Describe your travel request (destinations, dates, passengers)"; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isDesktop) - StepRow( - count: 4, - current: 3, - width: MediaQuery.of(context).size.width - 32, - ), - if (!isDesktop) const SizedBox(height: 14), - Text( - descriptionTitle, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox(height: isDesktop ? 16 : 8), - Text( - descriptionSubtitle, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - SizedBox(height: isDesktop ? 32 : 24), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _descriptionController, - focusNode: _descriptionFocusNode, - autocorrect: false, - enableSuggestions: false, - minLines: 3, - maxLines: 6, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - descriptionPlaceholder, - _descriptionFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - ), - ), - SizedBox(height: isDesktop ? 24 : 16), - - // Country picker (shared) - _buildCountryPicker(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - - // Privacy checkbox (shared) - _buildPrivacyCheckbox(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - - // Submit button (shared) - _buildSubmitButton(), - ], - ); - } - - // Travel form helpers. - Widget _buildTravelDropdown({ - required String? value, - required List items, - required String hint, - required ValueChanged onChanged, - required bool isDesktop, - }) { - return ClipRRect( - borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), - child: DropdownButtonHideUnderline( - child: DropdownButton2( - value: value, - items: items - .map( - (c) => DropdownMenuItem( - value: c, - child: Text( - c, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - ) - : STextStyles.w500_14(context), - ), - ), - ) - .toList(), - onChanged: onChanged, - hint: Text( - hint, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldDefaultSearchIconLeft, - ) - : STextStyles.fieldLabel(context), - ), - isExpanded: true, - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveSearchIconRight, - ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, 0), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), + ], ), ); } +} - Widget _buildTravelerCounter({ - required String label, - required int value, - required int min, - required int max, - required ValueChanged onChanged, - required bool isDesktop, - }) { - return Row( - children: [ - Text( - label, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - const Spacer(), - InkWell( - onTap: value > min ? () => onChanged(value - 1) : null, - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Center( - child: Text( - "-", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ), - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 24, - child: Center( - child: Text( - "$value", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ), - ), - const SizedBox(width: 16), - InkWell( - onTap: value < max ? () => onChanged(value + 1) : null, - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Center( - child: Text( - "+", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ), - ), - ), - ], - ); - } - - Widget _buildTravelContent(bool isDesktop) { - final departureCountryError = - _departureCountryTouched && - _departureCountryController.text.trim().isEmpty - ? "Required" - : null; - - final departureCityError = - _departureCityTouched && _departureCityController.text.trim().isEmpty - ? "Required" - : null; - - final destinationsError = - _destinationsTouched && - _destinationsController.text.trim().isEmpty && - !_needsRecommendations - ? "Required (or check 'I need recommendations')" - : null; - - final departureDateError = - _departureDateTouched && _departureDateController.text.trim().isEmpty - ? "Required" - : null; - - final returnDateError = - _returnDateTouched && _returnDateController.text.trim().isEmpty - ? "Required" - : null; - - final tripLengthError = - _tripLengthTouched && _tripLengthController.text.trim().isEmpty - ? "Required" - : null; - - final travelBudgetText = _travelBudgetController.text.trim(); - final travelBudgetVal = int.tryParse(travelBudgetText); - final travelBudgetError = - _travelBudgetTouched && - (travelBudgetText.isEmpty || - travelBudgetVal == null || - travelBudgetVal < 1000) - ? "Minimum budget is 1,000 EUR" - : null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isDesktop) - StepRow( - count: 4, - current: 3, - width: MediaQuery.of(context).size.width - 32, - ), - if (!isDesktop) const SizedBox(height: 14), - Text( - "Travel request", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox(height: isDesktop ? 16 : 8), - Text( - "Tell us about your trip and we'll arrange everything.", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - SizedBox(height: isDesktop ? 32 : 24), - - // === Trip Type === - Text( - "Trip type", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelDropdown( - value: _selectedArrangement, - items: const [ - "Flights Only", - "Hotels Only", - "Flights + Hotels", - "Full Service", - ], - hint: "Arrangement type", - onChanged: (val) => setState(() => _selectedArrangement = val), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 16 : 12), - TextField( - controller: _arrangementDetailsController, - focusNode: _arrangementDetailsFocusNode, - minLines: 3, - maxLines: 6, - autocorrect: false, - enableSuggestions: false, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Describe your specific requirements " - "(luggage, cabin class, hotel stars, etc.)", - _arrangementDetailsFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: - _arrangementDetailsTouched && - _arrangementDetailsController.text.trim().length < 10 - ? "Minimum 10 characters" - : null, - ), - ), - - // === Where === - SizedBox(height: isDesktop ? 24 : 16), - Text( - "Where", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildDepartureCountryPicker(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - TextField( - controller: _departureCityController, - focusNode: _departureCityFocusNode, - autocorrect: false, - enableSuggestions: false, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Departure city", - _departureCityFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: departureCityError, - ), - ), - SizedBox(height: isDesktop ? 16 : 12), - TextField( - controller: _destinationsController, - focusNode: _destinationsFocusNode, - autocorrect: false, - enableSuggestions: false, - enabled: !_needsRecommendations, - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "e.g. Paris, France; Rome, Italy", - _destinationsFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: destinationsError, - ), - ), - SizedBox(height: isDesktop ? 12 : 8), - GestureDetector( - onTap: () { - setState(() { - _needsRecommendations = !_needsRecommendations; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: IgnorePointer( - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _needsRecommendations, - onChanged: (_) {}, - ), - ), - ), - const SizedBox(width: 12), - Text( - "I need recommendations", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - ], - ), - ), - ), - - // === When === - SizedBox(height: isDesktop ? 24 : 16), - Text( - "When", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelDropdown( - value: _selectedDateMode, - items: const ["Exact dates", "Flexible dates"], - hint: "Date mode", - onChanged: (val) => setState(() => _selectedDateMode = val), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 16 : 12), - - if (_selectedDateMode == "Exact dates") ...[ - TextField( - controller: _departureDateController, - focusNode: _departureDateFocusNode, - readOnly: true, - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 3650)), - ); - if (picked != null) { - final formatted = - "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; - setState(() { - _departureDateController.text = formatted; - _departureDateTouched = true; - }); - } - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "DD/MM/YYYY", - _departureDateFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - labelText: "Departure date", - suffixIcon: const Icon(Icons.calendar_today, size: 18), - errorText: departureDateError, - ), - ), - SizedBox(height: isDesktop ? 16 : 12), - TextField( - controller: _returnDateController, - focusNode: _returnDateFocusNode, - readOnly: true, - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 3650)), - ); - if (picked != null) { - final formatted = - "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; - setState(() { - _returnDateController.text = formatted; - _returnDateTouched = true; - }); - } - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "DD/MM/YYYY", - _returnDateFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - labelText: "Return date", - suffixIcon: const Icon(Icons.calendar_today, size: 18), - errorText: returnDateError, - ), - ), - SizedBox(height: isDesktop ? 16 : 12), - _buildTravelDropdown( - value: _selectedFlexibility, - items: const [ - "Exact", - "\u00B1 1 day", - "\u00B1 2-3 days", - "+ 1 week", - ], - hint: "Flexibility", - onChanged: (val) => setState(() => _selectedFlexibility = val), - isDesktop: isDesktop, - ), - ], - - if (_selectedDateMode == "Flexible dates") ...[ - _buildTravelDropdown( - value: _selectedYear, - items: ["${DateTime.now().year}", "${DateTime.now().year + 1}"], - hint: "Year", - onChanged: (val) => setState(() => _selectedYear = val), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 16 : 12), - _buildTravelDropdown( - value: _selectedMonthSeason, - items: const [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ], - hint: "Month or season", - onChanged: (val) => setState(() => _selectedMonthSeason = val), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 16 : 12), - TextField( - controller: _tripLengthController, - focusNode: _tripLengthFocusNode, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Number of nights", - _tripLengthFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - errorText: tripLengthError, - ), - ), - ], - - // === Who === - SizedBox(height: isDesktop ? 24 : 16), - Text( - "Who", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelerCounter( - label: "Adults", - value: _adults, - min: 1, - max: 20, - onChanged: (v) => setState(() => _adults = v), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelerCounter( - label: "Children", - value: _children, - min: 0, - max: 20, - onChanged: (v) => setState(() => _children = v), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelerCounter( - label: "Infants", - value: _infants, - min: 0, - max: 20, - onChanged: (v) => setState(() => _infants = v), - isDesktop: isDesktop, - ), - SizedBox(height: isDesktop ? 12 : 8), - _buildTravelerCounter( - label: "Pets", - value: _pets, - min: 0, - max: 20, - onChanged: (v) => setState(() => _pets = v), - isDesktop: isDesktop, - ), - - // === Budget === - SizedBox(height: isDesktop ? 24 : 16), - Text( - "Budget", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w500_14(context), - ), - SizedBox(height: isDesktop ? 12 : 8), - TextField( - controller: _travelBudgetController, - focusNode: _travelBudgetFocusNode, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: - standardInputDecoration( - "Minimum 1000 EUR", - _travelBudgetFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixText: "EUR", - errorText: travelBudgetError, - ), - ), +class _ShopInBitStep4MobileShell extends StatelessWidget { + const _ShopInBitStep4MobileShell({required this.content}); - // Travel doesn't need delivery country: destinations are in the form. - SizedBox(height: isDesktop ? 16 : 12), - _buildPrivacyCheckbox(isDesktop), - SizedBox(height: isDesktop ? 16 : 12), - _buildSubmitButton(), - ], - ); - } + final Widget content; @override Widget build(BuildContext context) { - final isDesktop = Util.isDesktop; - - final Widget content; - switch (widget.model.category) { - case ShopInBitCategory.concierge: - content = _buildConciergeContent(isDesktop); - break; - case ShopInBitCategory.car: - content = _buildCarContent(isDesktop); - break; - case ShopInBitCategory.travel: - content = _buildTravelContent(isDesktop); - break; - case null: - content = _buildGenericContent(isDesktop); - break; - } - - if (isDesktop) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 750, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const AppBarBackButton(isCompact: true, iconSize: 23), - Text("ShopinBit", style: STextStyles.desktopH3(context)), - ], - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: SingleChildScrollView(child: content), - ), - ), - ], - ), - ); - } - return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, 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 new file mode 100644 index 000000000..bf4da508a --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart @@ -0,0 +1,347 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +import "../../../db/isar/main_db.dart"; +import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../themes/stack_colors.dart"; +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; +import "../../../widgets/rounded_white_container.dart"; +import "../shopinbit_car_fee_view.dart"; +import "../shopinbit_tickets_view.dart"; +import "shopinbit_country_picker.dart"; +import "shopinbit_labeled_checkbox.dart"; +import "shopinbit_privacy_checkbox.dart"; +import "shopinbit_step4_dropdown.dart"; +import "shopinbit_step4_header.dart"; +import "shopinbit_step4_submit_button.dart"; +import "shopinbit_step4_text_field.dart"; + +const List _carConditions = ["NEW", "PREOWNED"]; + +const int _minCarBudget = 20000; +const int _minCarFieldLength = 3; + +class ShopInBitCarResearchForm extends StatefulWidget { + const ShopInBitCarResearchForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + State createState() => + _ShopInBitCarResearchFormState(); +} + +class _ShopInBitCarResearchFormState extends State { + final TextEditingController _brandController = TextEditingController(); + final FocusNode _brandFocusNode = FocusNode(); + bool _brandTouched = false; + + final TextEditingController _modelController = TextEditingController(); + final FocusNode _modelFocusNode = FocusNode(); + bool _modelTouched = false; + + final TextEditingController _carDescriptionController = + TextEditingController(); + final FocusNode _carDescriptionFocusNode = FocusNode(); + bool _carDescriptionTouched = false; + + final TextEditingController _carBudgetController = TextEditingController(); + final FocusNode _carBudgetFocusNode = FocusNode(); + bool _carBudgetTouched = false; + + String? _selectedCarCondition; + bool _feeAcknowledged = false; + String? _selectedCountryIso; + bool _privacyAccepted = false; + bool _submitting = false; + + @override + void initState() { + super.initState(); + _wireTouchOnBlur(_brandFocusNode, () => _brandTouched = true); + _wireTouchOnBlur(_modelFocusNode, () => _modelTouched = true); + _wireTouchOnBlur( + _carDescriptionFocusNode, + () => _carDescriptionTouched = true, + ); + _wireTouchOnBlur(_carBudgetFocusNode, () => _carBudgetTouched = true); + if (widget.model.deliveryCountry.isNotEmpty) { + _selectedCountryIso = widget.model.deliveryCountry; + } + } + + void _wireTouchOnBlur(FocusNode node, VoidCallback markTouched) { + node.addListener(() { + if (!node.hasFocus) markTouched(); + setState(() {}); + }); + } + + @override + void dispose() { + _brandController.dispose(); + _brandFocusNode.dispose(); + _modelController.dispose(); + _modelFocusNode.dispose(); + _carDescriptionController.dispose(); + _carDescriptionFocusNode.dispose(); + _carBudgetController.dispose(); + _carBudgetFocusNode.dispose(); + super.dispose(); + } + + bool get _canContinue { + final int? carBudgetValue = int.tryParse(_carBudgetController.text.trim()); + return !_submitting && + _privacyAccepted && + _feeAcknowledged && + _brandController.text.trim().length >= _minCarFieldLength && + _modelController.text.trim().length >= _minCarFieldLength && + _carDescriptionController.text.trim().length >= _minCarFieldLength && + _selectedCarCondition != null && + carBudgetValue != null && + carBudgetValue >= _minCarBudget && + _selectedCountryIso != null; + } + + Future _submit() async { + setState(() => _submitting = true); + try { + final String countryIso = _selectedCountryIso!; + + widget.model + ..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" + ..deliveryCountry = countryIso; + + // Block if another car research flow is already in progress. + final existingPending = MainDB.instance + .getShopInBitTickets() + .where((t) => t.isPendingPayment) + .toList(); + + if (existingPending.isNotEmpty && mounted) { + final bool? resumePrevious = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text("In-Progress Car Research"), + content: const Text( + "You have an unfinished car research payment. " + "Would you like to resume it or start a new search?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text("Resume Previous"), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text("Start New"), + ), + ], + ), + ); + + if (resumePrevious == true && mounted) { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + ShopInBitTicketsView.routeName, + (route) => route.isFirst, + ), + ); + return; + } + } + + if (!mounted) return; + + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showDialog( + context: context, + builder: (_) => ShopInBitCarFeeView(model: widget.model), + ), + ); + } else { + unawaited( + Navigator.of( + context, + ).pushNamed(ShopInBitCarFeeView.routeName, arguments: widget.model), + ); + } + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + + final String? brandError = + _brandTouched && + _brandController.text.trim().length < _minCarFieldLength + ? "Minimum $_minCarFieldLength characters" + : null; + + final String? modelError = + _modelTouched && + _modelController.text.trim().length < _minCarFieldLength + ? "Minimum $_minCarFieldLength characters" + : null; + + final String? carDescriptionError = + _carDescriptionTouched && + _carDescriptionController.text.trim().length < _minCarFieldLength + ? "Minimum $_minCarFieldLength characters" + : null; + + final String carBudgetText = _carBudgetController.text.trim(); + final int? carBudgetValue = int.tryParse(carBudgetText); + final String? carBudgetError = + _carBudgetTouched && + (carBudgetText.isEmpty || + carBudgetValue == null || + carBudgetValue < _minCarBudget) + ? "Minimum budget is 20,000\u20AC" + : null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const ShopInBitStep4Header( + title: "Car Research request", + subtitle: "Tell us about the car you're looking for.", + ), + SizedBox(height: isDesktop ? 32 : 24), + ShopInBitCountryPicker( + selectedIso: _selectedCountryIso, + onChanged: (iso) => setState(() => _selectedCountryIso = iso), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4TextField( + controller: _brandController, + focusNode: _brandFocusNode, + hintText: "Car brand (e.g., BMW, Mercedes, Toyota...)", + errorText: brandError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4TextField( + controller: _modelController, + focusNode: _modelFocusNode, + hintText: "Car model (e.g., 3 Series, E-Class, Camry...)", + errorText: modelError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4Dropdown( + value: _selectedCarCondition, + items: _carConditions, + hintText: "Condition", + onChanged: (value) => setState(() => _selectedCarCondition = value), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4TextField( + controller: _carDescriptionController, + focusNode: _carDescriptionFocusNode, + hintText: + "Describe your requirements " + "(year, mileage, features...)", + minLines: 3, + maxLines: 6, + errorText: carDescriptionError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4TextField( + controller: _carBudgetController, + focusNode: _carBudgetFocusNode, + hintText: "Budget (\u20AC, minimum 20,000)", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + suffixText: "\u20AC", + errorText: carBudgetError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + _CarResearchFeeInfo(isDesktop: isDesktop), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitLabeledCheckbox( + value: _feeAcknowledged, + onChanged: (v) => setState(() => _feeAcknowledged = v), + label: "I acknowledge the \u20AC223 research fee", + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitPrivacyCheckbox( + value: _privacyAccepted, + onChanged: (v) => setState(() => _privacyAccepted = v), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4SubmitButton( + submitting: _submitting, + enabled: _canContinue, + onPressed: _submit, + ), + ], + ); + } +} + +/// Info box showing the €223 (incl. VAT) research fee disclosure. +class _CarResearchFeeInfo extends StatelessWidget { + const _CarResearchFeeInfo({required this.isDesktop}); + + final bool isDesktop; + + @override + Widget build(BuildContext context) { + final TextStyle baseStyle = isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w500_14(context); + + return RoundedWhiteContainer( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + size: 20, + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 12), + Expanded( + child: RichText( + text: TextSpan( + style: baseStyle, + children: [ + TextSpan( + text: "Research fee: ", + style: baseStyle.copyWith(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: + "\u20AC223 (incl. VAT): one-time payment, " + "credited toward your purchase.", + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart new file mode 100644 index 000000000..282d82613 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart @@ -0,0 +1,191 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../utilities/util.dart"; +import "shopinbit_country_picker.dart"; +import "shopinbit_labeled_checkbox.dart"; +import "shopinbit_privacy_checkbox.dart"; +import "shopinbit_step4_dropdown.dart"; +import "shopinbit_step4_header.dart"; +import "shopinbit_step4_submit.dart"; +import "shopinbit_step4_submit_button.dart"; +import "shopinbit_step4_text_field.dart"; + +const List _conciergeConditions = ["NEW", "USED"]; + +const int _minConciergeBudget = 1000; +const int _maxConciergeBudget = 100000; + +class ShopInBitConciergeForm extends StatefulWidget { + const ShopInBitConciergeForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + State createState() => _ShopInBitConciergeFormState(); +} + +class _ShopInBitConciergeFormState extends State { + final TextEditingController _whatToPurchaseController = + TextEditingController(); + final FocusNode _whatToPurchaseFocusNode = FocusNode(); + bool _whatToPurchaseTouched = false; + + final TextEditingController _budgetController = TextEditingController( + text: "1000", + ); + final FocusNode _budgetFocusNode = FocusNode(); + bool _budgetTouched = false; + + String? _selectedCondition; + bool _noLimit = false; + String? _selectedCountryIso; + bool _privacyAccepted = false; + bool _submitting = false; + + @override + void initState() { + super.initState(); + _whatToPurchaseFocusNode.addListener(() { + if (!_whatToPurchaseFocusNode.hasFocus) _whatToPurchaseTouched = true; + setState(() {}); + }); + _budgetFocusNode.addListener(() { + if (!_budgetFocusNode.hasFocus) _budgetTouched = true; + setState(() {}); + }); + if (widget.model.deliveryCountry.isNotEmpty) { + _selectedCountryIso = widget.model.deliveryCountry; + } + } + + @override + void dispose() { + _whatToPurchaseController.dispose(); + _whatToPurchaseFocusNode.dispose(); + _budgetController.dispose(); + _budgetFocusNode.dispose(); + super.dispose(); + } + + bool get _budgetIsValid { + final String text = _budgetController.text.trim(); + if (text.isEmpty) return false; + final int? value = int.tryParse(text); + return value != null && + value >= _minConciergeBudget && + value <= _maxConciergeBudget; + } + + bool get _canContinue => + !_submitting && + _privacyAccepted && + _whatToPurchaseController.text.trim().length >= 10 && + _selectedCondition != null && + (_noLimit || _budgetIsValid) && + _selectedCountryIso != null; + + Future _submit() async { + setState(() => _submitting = true); + + final String countryIso = _selectedCountryIso!; + final String budgetText = _noLimit + ? "No limit" + : "${_budgetController.text.trim()} EUR"; + + widget.model + ..requestDescription = + "What to purchase: ${_whatToPurchaseController.text.trim()}\n" + "Condition: $_selectedCondition\n" + "Budget: $budgetText\n" + "Delivery country: $countryIso" + ..deliveryCountry = countryIso; + + try { + await submitShopInBitRequest(context, widget.model); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + + final String? whatToPurchaseError = + _whatToPurchaseTouched && + _whatToPurchaseController.text.trim().length < 10 + ? "Minimum 10 characters" + : null; + + final String? budgetError = _budgetTouched && !_noLimit && !_budgetIsValid + ? "Enter a value between 1,000 and 100,000" + : null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const ShopInBitStep4Header( + title: "What would you like to purchase?", + subtitle: + "Tell us what you're looking for and we'll find it " + "for you.", + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _whatToPurchaseController, + focusNode: _whatToPurchaseFocusNode, + hintText: + "Describe what you'd like to purchase " + "(e.g., electronics, luxury goods, services...)", + minLines: 3, + maxLines: 6, + errorText: whatToPurchaseError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4Dropdown( + value: _selectedCondition, + items: _conciergeConditions, + hintText: "Condition", + onChanged: (value) => setState(() => _selectedCondition = value), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitStep4TextField( + controller: _budgetController, + focusNode: _budgetFocusNode, + hintText: "Budget (\u20AC)", + enabled: !_noLimit, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + suffixText: "\u20AC", + errorText: budgetError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitLabeledCheckbox( + value: _noLimit, + onChanged: (v) => setState(() => _noLimit = v), + label: "No budget limit", + ), + SizedBox(height: isDesktop ? 12 : 12), + ShopInBitCountryPicker( + selectedIso: _selectedCountryIso, + onChanged: (iso) => setState(() => _selectedCountryIso = iso), + ), + SizedBox(height: isDesktop ? 12 : 12), + ShopInBitPrivacyCheckbox( + value: _privacyAccepted, + onChanged: (v) => setState(() => _privacyAccepted = v), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4SubmitButton( + submitting: _submitting, + enabled: _canContinue, + onPressed: _submit, + ), + ], + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart new file mode 100644 index 000000000..f5ae405b2 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart @@ -0,0 +1,164 @@ +import "package:dropdown_button2/dropdown_button2.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/svg.dart"; + +import "../../../services/shopinbit/shopinbit_service.dart"; +import "../../../themes/stack_colors.dart"; +import "../../../utilities/assets.dart"; +import "../../../utilities/constants.dart"; +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; + +class ShopInBitCountryPicker extends StatefulWidget { + const ShopInBitCountryPicker({ + super.key, + required this.selectedIso, + required this.onChanged, + this.hintText = "Delivery country", + }); + + final String? selectedIso; + final ValueChanged onChanged; + final String hintText; + + @override + State createState() => _ShopInBitCountryPickerState(); +} + +class _ShopInBitCountryPickerState extends State { + final TextEditingController _searchController = TextEditingController(); + List> _countries = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + _fetchCountries(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _fetchCountries() async { + setState(() => _loading = true); + try { + final resp = await ShopInBitService.instance.client.getCountries(); + if (resp.hasError || resp.value == null) return; + _countries = resp.value!; + if (widget.selectedIso != null && + !_countries.any((c) => c["iso"] == widget.selectedIso)) { + widget.onChanged(null); + } + } catch (_) { + // Leave list empty; user will see no items. + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + final StackColors stackColors = Theme.of(context).extension()!; + + 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 ClipRRect( + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + child: DropdownButtonHideUnderline( + child: DropdownButton2( + value: widget.selectedIso, + items: _countries + .map( + (c) => DropdownMenuItem( + value: c["iso"] as String, + child: Text(c["label"] as String, style: itemStyle), + ), + ) + .toList(), + onMenuStateChange: (isOpen) { + if (!isOpen) { + _searchController.clear(); + } + }, + onChanged: _loading ? null : widget.onChanged, + hint: Text( + _loading ? "Loading countries..." : widget.hintText, + 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, 0), + elevation: 0, + maxHeight: 300, + decoration: BoxDecoration( + color: stackColors.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + dropdownSearchData: DropdownSearchData( + 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) { + final String? 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), + ), + ), + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart new file mode 100644 index 000000000..f31741f19 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart @@ -0,0 +1,112 @@ +import "package:flutter/material.dart"; + +import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../utilities/util.dart"; +import "shopinbit_country_picker.dart"; +import "shopinbit_privacy_checkbox.dart"; +import "shopinbit_step4_header.dart"; +import "shopinbit_step4_submit.dart"; +import "shopinbit_step4_submit_button.dart"; +import "shopinbit_step4_text_field.dart"; + +/// Fallback Step 4 form used when no category was selected. Collects a free +/// text description and a delivery country. +/// +/// Note: the original code used the travel copy for this fallback; that +/// behaviour is preserved here. +class ShopInBitGenericForm extends StatefulWidget { + const ShopInBitGenericForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + State createState() => _ShopInBitGenericFormState(); +} + +class _ShopInBitGenericFormState extends State { + late final TextEditingController _descriptionController; + final FocusNode _descriptionFocusNode = FocusNode(); + + String? _selectedCountryIso; + bool _privacyAccepted = false; + bool _submitting = false; + + @override + void initState() { + super.initState(); + _descriptionController = TextEditingController( + text: widget.model.requestDescription, + ); + _descriptionFocusNode.addListener(() => setState(() {})); + + if (widget.model.deliveryCountry.isNotEmpty) { + _selectedCountryIso = widget.model.deliveryCountry; + } + } + + @override + void dispose() { + _descriptionController.dispose(); + _descriptionFocusNode.dispose(); + super.dispose(); + } + + bool get _canContinue => + !_submitting && + _privacyAccepted && + _descriptionController.text.trim().isNotEmpty && + _selectedCountryIso != null; + + Future _submit() async { + setState(() => _submitting = true); + widget.model + ..requestDescription = _descriptionController.text.trim() + ..deliveryCountry = _selectedCountryIso!; + try { + await submitShopInBitRequest(context, widget.model); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const ShopInBitStep4Header( + title: "Describe your travel request", + subtitle: "Provide details about your trip.", + ), + SizedBox(height: isDesktop ? 32 : 24), + ShopInBitStep4TextField( + controller: _descriptionController, + focusNode: _descriptionFocusNode, + hintText: + "Describe your travel request (destinations, dates, passengers)", + minLines: 3, + maxLines: 6, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 24 : 16), + ShopInBitCountryPicker( + selectedIso: _selectedCountryIso, + onChanged: (iso) => setState(() => _selectedCountryIso = iso), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitPrivacyCheckbox( + value: _privacyAccepted, + onChanged: (v) => setState(() => _privacyAccepted = v), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4SubmitButton( + submitting: _submitting, + enabled: _canContinue, + onPressed: _submit, + ), + ], + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_labeled_checkbox.dart b/lib/pages/shopinbit/step_4_components/shopinbit_labeled_checkbox.dart new file mode 100644 index 000000000..6f4014f88 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_labeled_checkbox.dart @@ -0,0 +1,49 @@ +import "package:flutter/material.dart"; + +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; + +class ShopInBitLabeledCheckbox extends StatelessWidget { + const ShopInBitLabeledCheckbox({ + super.key, + required this.value, + required this.onChanged, + required this.label, + }); + + final bool value; + final ValueChanged onChanged; + final String label; + + @override + Widget build(BuildContext context) { + final TextStyle labelStyle = Util.isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w500_14(context); + + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: IgnorePointer( + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: value, + onChanged: (_) {}, + ), + ), + ), + const SizedBox(width: 12), + Expanded(child: Text(label, style: labelStyle)), + ], + ), + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart new file mode 100644 index 000000000..72d95050d --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart @@ -0,0 +1,163 @@ +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:url_launcher/url_launcher.dart"; + +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; +import "../../../widgets/desktop/desktop_dialog.dart"; +import "../../../widgets/desktop/primary_button.dart"; +import "../../../widgets/desktop/secondary_button.dart"; +import "../../../widgets/stack_dialog.dart"; + +const String _shopInBitPrivacyUrl = + "https://api.shopinbit.com/static/policy/privacy.html"; + +class ShopInBitPrivacyCheckbox extends StatelessWidget { + const ShopInBitPrivacyCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + final bool value; + final ValueChanged onChanged; + + Future _openPrivacyPolicy(BuildContext context) async { + final bool shouldOpen = await _showOpenBrowserWarning( + context, + _shopInBitPrivacyUrl, + ); + if (shouldOpen) { + await launchUrl( + Uri.parse(_shopInBitPrivacyUrl), + mode: LaunchMode.externalApplication, + ); + } + } + + Future _showOpenBrowserWarning(BuildContext context, String url) async { + final Uri uri = Uri.parse(url); + final String message = + "You are about to open ${uri.scheme}://${uri.host} in your browser."; + + final bool? shouldContinue = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Util.isDesktop + ? _DesktopBrowserWarning(message: message) + : StackDialog( + title: "Attention", + message: message, + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), + ), + rightButton: PrimaryButton( + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ); + return shouldContinue ?? false; + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: isDesktop ? 3 : 0), + child: SizedBox( + width: 20, + height: 20, + child: IgnorePointer( + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: value, + onChanged: (_) {}, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: RichText( + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w500_14(context), + children: [ + const TextSpan( + text: "I have read and agree to the ShopinBit ", + ), + TextSpan( + text: "Privacy Policy", + style: STextStyles.richLink( + context, + ).copyWith(fontSize: isDesktop ? 18 : 14), + recognizer: TapGestureRecognizer() + ..onTap = () => _openPrivacyPolicy(context), + ), + const TextSpan(text: "."), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _DesktopBrowserWarning extends StatelessWidget { + const _DesktopBrowserWarning({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 550, + maxHeight: 250, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Text("Attention", style: STextStyles.desktopH2(context)), + const SizedBox(height: 16), + Text(message, style: STextStyles.desktopTextSmall(context)), + const SizedBox(height: 35), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_dropdown.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_dropdown.dart new file mode 100644 index 000000000..baae09287 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_dropdown.dart @@ -0,0 +1,93 @@ +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"; + +class ShopInBitStep4Dropdown extends StatelessWidget { + const ShopInBitStep4Dropdown({ + super.key, + required this.value, + required this.items, + required this.hintText, + required this.onChanged, + }); + + final String? value; + final List items; + final String hintText; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final stackColors = Theme.of(context).extension()!; + + final itemStyle = Util.isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith(color: stackColors.textFieldActiveText) + : STextStyles.w500_14(context); + + final hintStyle = Util.isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith(color: stackColors.textFieldDefaultSearchIconLeft) + : STextStyles.fieldLabel(context); + + return ClipRRect( + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + child: DropdownButtonHideUnderline( + child: DropdownButton2( + value: value, + items: items + .map( + (item) => DropdownMenuItem( + value: item, + child: Text(item, style: itemStyle), + ), + ) + .toList(), + onChanged: onChanged, + hint: Text(hintText, 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, 0), + elevation: 0, + decoration: BoxDecoration( + color: stackColors.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_header.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_header.dart new file mode 100644 index 000000000..4c81d7df1 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_header.dart @@ -0,0 +1,46 @@ +import "package:flutter/material.dart"; + +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; +import "../../exchange_view/sub_widgets/step_row.dart"; + +class ShopInBitStep4Header extends StatelessWidget { + const ShopInBitStep4Header({ + super.key, + required this.title, + required this.subtitle, + }); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) ...[ + StepRow( + count: 4, + current: 3, + width: MediaQuery.of(context).size.width - 32, + ), + const SizedBox(height: 14), + ], + Text( + title, + style: Util.isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + Text( + subtitle, + style: Util.isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle(context), + ), + ], + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart new file mode 100644 index 000000000..ede142409 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart @@ -0,0 +1,96 @@ +import "dart:async"; + +import "package:flutter/material.dart"; + +import "../../../db/isar/main_db.dart"; +import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../notifications/show_flush_bar.dart"; +import "../../../services/shopinbit/shopinbit_service.dart"; +import "../../../utilities/util.dart"; +import "../shopinbit_order_created.dart"; + +/// Submits a ShopinBit request to the API and navigates to the order-created +/// view on success. +/// +/// Used by the concierge, travel and generic flows. The car flow has its own +/// pre-payment branching (fee view) and does not call this helper. +Future submitShopInBitRequest( + BuildContext context, + ShopInBitOrderModel model, +) async { + try { + final ShopInBitService service = ShopInBitService.instance; + final String customerKey = await service.ensureCustomerKey(); + + assert( + model.category != null, + "Step 4 reached with null category: Step 2 must set category before" + " reaching Step 4", + ); + + // API service_type: travel requests use "concierge" because the + // ShopinBit API routes both through the same concierge pipeline. + // Travel-specific details are captured in the structured comment field. + final String categoryStr = switch (model.category) { + ShopInBitCategory.concierge => "concierge", + ShopInBitCategory.travel => "concierge", + ShopInBitCategory.car => "car", + null => throw StateError("category must be non-null at Step 4 submit"), + }; + + final resp = await service.client.createRequest( + customerPseudonym: model.displayName, + externalCustomerKey: customerKey, + serviceType: categoryStr, + comment: model.requestDescription, + deliveryCountry: model.deliveryCountry, + ); + + if (resp.hasError) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: resp.exception?.message ?? "Failed to create request", + context: context, + ), + ); + } + return; + } + + final ref = resp.value!; + model + ..apiTicketId = ref.id + ..ticketId = ref.number + ..status = ShopInBitOrderStatus.pending; + await MainDB.instance.putShopInBitTicket(model.toIsarTicket()); + + if (!context.mounted) return; + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showDialog( + context: context, + builder: (_) => ShopInBitOrderCreated(model: model), + ), + ); + } else { + unawaited( + Navigator.of( + context, + ).pushNamed(ShopInBitOrderCreated.routeName, arguments: model), + ); + } + } catch (e) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to create request: $e", + context: context, + ), + ); + } + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit_button.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit_button.dart new file mode 100644 index 000000000..ac38c46bb --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit_button.dart @@ -0,0 +1,25 @@ +import "package:flutter/material.dart"; + +import "../../../widgets/desktop/primary_button.dart"; + +class ShopInBitStep4SubmitButton extends StatelessWidget { + const ShopInBitStep4SubmitButton({ + super.key, + required this.submitting, + required this.enabled, + required this.onPressed, + }); + + final bool submitting; + final bool enabled; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return PrimaryButton( + label: submitting ? "Submitting..." : "Submit request", + enabled: enabled, + onPressed: enabled ? onPressed : null, + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_text_field.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_text_field.dart new file mode 100644 index 000000000..7cdc97a30 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_text_field.dart @@ -0,0 +1,89 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +import "../../../themes/stack_colors.dart"; +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; +import "../../../widgets/stack_text_field.dart"; + +class ShopInBitStep4TextField extends StatelessWidget { + const ShopInBitStep4TextField({ + super.key, + required this.controller, + required this.focusNode, + required this.hintText, + this.errorText, + this.minLines, + this.maxLines = 1, + this.keyboardType, + this.inputFormatters, + this.enabled = true, + this.suffixText, + this.suffixIcon, + this.labelText, + this.readOnly = false, + this.onTap, + this.onChanged, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final String hintText; + final String? errorText; + final int? minLines; + final int? maxLines; + final TextInputType? keyboardType; + final List? inputFormatters; + final bool enabled; + final String? suffixText; + final Widget? suffixIcon; + final String? labelText; + final bool readOnly; + final VoidCallback? onTap; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + final TextStyle style = Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context); + + return TextField( + controller: controller, + focusNode: focusNode, + autocorrect: false, + enableSuggestions: false, + enabled: enabled, + readOnly: readOnly, + onTap: onTap, + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + onChanged: onChanged, + style: style, + decoration: + standardInputDecoration( + hintText, + focusNode, + context, + desktopMed: Util.isDesktop, + ).copyWith( + filled: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + errorText: errorText, + suffixText: suffixText, + suffixIcon: suffixIcon, + labelText: labelText, + ), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart new file mode 100644 index 000000000..bfddd184a --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart @@ -0,0 +1,544 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; +import "shopinbit_country_picker.dart"; +import "shopinbit_labeled_checkbox.dart"; +import "shopinbit_privacy_checkbox.dart"; +import "shopinbit_step4_dropdown.dart"; +import "shopinbit_step4_header.dart"; +import "shopinbit_step4_submit.dart"; +import "shopinbit_step4_submit_button.dart"; +import "shopinbit_step4_text_field.dart"; +import "shopinbit_traveler_counter.dart"; + +const String _exactDates = "Exact dates"; +const String _flexibleDates = "Flexible dates"; + +const List _arrangements = [ + "Flights Only", + "Hotels Only", + "Flights + Hotels", + "Full Service", +]; + +const List _dateModes = [_exactDates, _flexibleDates]; + +const List _flexibilities = [ + "Exact", + "\u00B1 1 day", + "\u00B1 2-3 days", + "+ 1 week", +]; + +const List _months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +const int _minTravelBudget = 1000; +const int _minArrangementDetailsLength = 10; + +/// Travel request form. Collects arrangement type, departure / destinations, +/// dates (either exact or flexible), travelers and budget, then submits via +/// the shared submit helper. +class ShopInBitTravelForm extends StatefulWidget { + const ShopInBitTravelForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + State createState() => _ShopInBitTravelFormState(); +} + +class _ShopInBitTravelFormState extends State { + final TextEditingController _arrangementDetailsController = + TextEditingController(); + final FocusNode _arrangementDetailsFocusNode = FocusNode(); + bool _arrangementDetailsTouched = false; + + final TextEditingController _departureCityController = + TextEditingController(); + final FocusNode _departureCityFocusNode = FocusNode(); + bool _departureCityTouched = false; + + final TextEditingController _destinationsController = TextEditingController(); + final FocusNode _destinationsFocusNode = FocusNode(); + bool _destinationsTouched = false; + + final TextEditingController _departureDateController = + TextEditingController(); + final FocusNode _departureDateFocusNode = FocusNode(); + bool _departureDateTouched = false; + + final TextEditingController _returnDateController = TextEditingController(); + final FocusNode _returnDateFocusNode = FocusNode(); + bool _returnDateTouched = false; + + final TextEditingController _tripLengthController = TextEditingController(); + final FocusNode _tripLengthFocusNode = FocusNode(); + bool _tripLengthTouched = false; + + final TextEditingController _travelBudgetController = TextEditingController( + text: "5000", + ); + final FocusNode _travelBudgetFocusNode = FocusNode(); + bool _travelBudgetTouched = false; + + String? _selectedArrangement; + String? _selectedDepartureCountryIso; + String? _selectedDateMode; + String? _selectedFlexibility; + String? _selectedYear; + String? _selectedMonthSeason; + bool _needsRecommendations = false; + + int _adults = 1; + int _children = 0; + int _infants = 0; + int _pets = 0; + + bool _privacyAccepted = false; + bool _submitting = false; + + @override + void initState() { + super.initState(); + _wireTouchOnBlur( + _arrangementDetailsFocusNode, + () => _arrangementDetailsTouched = true, + ); + _wireTouchOnBlur( + _departureCityFocusNode, + () => _departureCityTouched = true, + ); + _wireTouchOnBlur(_destinationsFocusNode, () => _destinationsTouched = true); + _wireTouchOnBlur( + _departureDateFocusNode, + () => _departureDateTouched = true, + ); + _wireTouchOnBlur(_returnDateFocusNode, () => _returnDateTouched = true); + _wireTouchOnBlur(_tripLengthFocusNode, () => _tripLengthTouched = true); + _wireTouchOnBlur(_travelBudgetFocusNode, () => _travelBudgetTouched = true); + } + + void _wireTouchOnBlur(FocusNode node, VoidCallback markTouched) { + node.addListener(() { + if (!node.hasFocus) markTouched(); + setState(() {}); + }); + } + + @override + void dispose() { + _arrangementDetailsController.dispose(); + _arrangementDetailsFocusNode.dispose(); + _departureCityController.dispose(); + _departureCityFocusNode.dispose(); + _destinationsController.dispose(); + _destinationsFocusNode.dispose(); + _departureDateController.dispose(); + _departureDateFocusNode.dispose(); + _returnDateController.dispose(); + _returnDateFocusNode.dispose(); + _tripLengthController.dispose(); + _tripLengthFocusNode.dispose(); + _travelBudgetController.dispose(); + _travelBudgetFocusNode.dispose(); + super.dispose(); + } + + bool get _hasValidDates => switch (_selectedDateMode) { + _flexibleDates => + _selectedYear != null && + _selectedMonthSeason != null && + _tripLengthController.text.trim().isNotEmpty, + _exactDates => + _departureDateController.text.trim().isNotEmpty && + _returnDateController.text.trim().isNotEmpty, + _ => false, + }; + + bool get _canContinue { + final int? travelBudgetValue = int.tryParse( + _travelBudgetController.text.trim(), + ); + return !_submitting && + _privacyAccepted && + _selectedArrangement != null && + _arrangementDetailsController.text.trim().length >= + _minArrangementDetailsLength && + _selectedDepartureCountryIso != null && + _departureCityController.text.trim().isNotEmpty && + (_needsRecommendations || + _destinationsController.text.trim().isNotEmpty) && + _selectedDateMode != null && + _hasValidDates && + _adults >= 1 && + travelBudgetValue != null && + travelBudgetValue >= _minTravelBudget; + } + + Future _pickDate( + TextEditingController target, + VoidCallback onPicked, + ) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (picked != null) { + setState(() { + target.text = _formatDate(picked); + onPicked(); + }); + } + } + + String _formatDate(DateTime date) { + final String day = date.day.toString().padLeft(2, "0"); + final String month = date.month.toString().padLeft(2, "0"); + return "$day/$month/${date.year}"; + } + + String _buildRequestDescription() { + final List parts = [ + "Arrangement: $_selectedArrangement", + "Details: ${_arrangementDetailsController.text.trim()}", + "Departure: ${_departureCityController.text.trim()}, " + "${_selectedDepartureCountryIso ?? ''}", + ]; + + if (_needsRecommendations) { + parts.add("Destinations: Recommendations requested"); + } else { + parts.add("Destinations: ${_destinationsController.text.trim()}"); + } + + if (_selectedDateMode == _exactDates) { + final String flex = + _selectedFlexibility != null && _selectedFlexibility != "Exact" + ? " ($_selectedFlexibility)" + : ""; + parts.add( + "Dates: ${_departureDateController.text.trim()} - " + "${_returnDateController.text.trim()}$flex", + ); + } else if (_selectedDateMode == _flexibleDates) { + parts.add( + "Dates: $_selectedMonthSeason $_selectedYear, " + "${_tripLengthController.text.trim()} nights", + ); + } + + final List travelers = ["$_adults adult${_adults > 1 ? 's' : ''}"]; + if (_children > 0) { + travelers.add("$_children child${_children > 1 ? 'ren' : ''}"); + } + if (_infants > 0) { + travelers.add("$_infants infant${_infants > 1 ? 's' : ''}"); + } + if (_pets > 0) { + travelers.add("$_pets pet${_pets > 1 ? 's' : ''}"); + } + parts.add("Travelers: ${travelers.join(', ')}"); + + parts.add("Budget: ${_travelBudgetController.text.trim()} EUR"); + + return parts.join("\n"); + } + + Future _submit() async { + setState(() => _submitting = true); + widget.model + ..requestDescription = _buildRequestDescription() + // Travel doesn't collect a delivery country: default to "DE" since the + // API requires the field. Travel destinations are captured in the + // structured comment field. + ..deliveryCountry = "DE"; + try { + await submitShopInBitRequest(context, widget.model); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + + final String? arrangementDetailsError = + _arrangementDetailsTouched && + _arrangementDetailsController.text.trim().length < + _minArrangementDetailsLength + ? "Minimum $_minArrangementDetailsLength characters" + : null; + + final String? departureCityError = + _departureCityTouched && _departureCityController.text.trim().isEmpty + ? "Required" + : null; + + final String? destinationsError = + _destinationsTouched && + !_needsRecommendations && + _destinationsController.text.trim().isEmpty + ? "Required (or check 'I need recommendations')" + : null; + + final String? departureDateError = + _departureDateTouched && _departureDateController.text.trim().isEmpty + ? "Required" + : null; + + final String? returnDateError = + _returnDateTouched && _returnDateController.text.trim().isEmpty + ? "Required" + : null; + + final String? tripLengthError = + _tripLengthTouched && _tripLengthController.text.trim().isEmpty + ? "Required" + : null; + + final String travelBudgetText = _travelBudgetController.text.trim(); + final int? travelBudgetValue = int.tryParse(travelBudgetText); + final String? travelBudgetError = + _travelBudgetTouched && + (travelBudgetText.isEmpty || + travelBudgetValue == null || + travelBudgetValue < _minTravelBudget) + ? "Minimum budget is 1,000 EUR" + : null; + + final int currentYear = DateTime.now().year; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const ShopInBitStep4Header( + title: "Travel request", + subtitle: "Tell us about your trip and we'll arrange everything.", + ), + SizedBox(height: isDesktop ? 32 : 24), + + _TravelSectionLabel(text: "Trip type", isDesktop: isDesktop), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitStep4Dropdown( + value: _selectedArrangement, + items: _arrangements, + hintText: "Arrangement type", + onChanged: (value) => setState(() => _selectedArrangement = value), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _arrangementDetailsController, + focusNode: _arrangementDetailsFocusNode, + hintText: + "Describe your specific requirements " + "(luggage, cabin class, hotel stars, etc.)", + minLines: 3, + maxLines: 6, + errorText: arrangementDetailsError, + onChanged: (_) => setState(() {}), + ), + + SizedBox(height: isDesktop ? 24 : 16), + _TravelSectionLabel(text: "Where", isDesktop: isDesktop), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitCountryPicker( + selectedIso: _selectedDepartureCountryIso, + onChanged: (iso) => + setState(() => _selectedDepartureCountryIso = iso), + hintText: "Departure country", + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _departureCityController, + focusNode: _departureCityFocusNode, + hintText: "Departure city", + errorText: departureCityError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _destinationsController, + focusNode: _destinationsFocusNode, + hintText: "e.g. Paris, France; Rome, Italy", + enabled: !_needsRecommendations, + errorText: destinationsError, + onChanged: (_) => setState(() {}), + ), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitLabeledCheckbox( + value: _needsRecommendations, + onChanged: (v) => setState(() => _needsRecommendations = v), + label: "I need recommendations", + ), + + SizedBox(height: isDesktop ? 24 : 16), + _TravelSectionLabel(text: "When", isDesktop: isDesktop), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitStep4Dropdown( + value: _selectedDateMode, + items: _dateModes, + hintText: "Date mode", + onChanged: (value) => setState(() => _selectedDateMode = value), + ), + SizedBox(height: isDesktop ? 16 : 12), + + if (_selectedDateMode == _exactDates) ...[ + ShopInBitStep4TextField( + controller: _departureDateController, + focusNode: _departureDateFocusNode, + hintText: "DD/MM/YYYY", + labelText: "Departure date", + readOnly: true, + onTap: () => _pickDate( + _departureDateController, + () => _departureDateTouched = true, + ), + suffixIcon: const Icon(Icons.calendar_today, size: 18), + errorText: departureDateError, + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _returnDateController, + focusNode: _returnDateFocusNode, + hintText: "DD/MM/YYYY", + labelText: "Return date", + readOnly: true, + onTap: () => _pickDate( + _returnDateController, + () => _returnDateTouched = true, + ), + suffixIcon: const Icon(Icons.calendar_today, size: 18), + errorText: returnDateError, + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4Dropdown( + value: _selectedFlexibility, + items: _flexibilities, + hintText: "Flexibility", + onChanged: (value) => setState(() => _selectedFlexibility = value), + ), + ], + + if (_selectedDateMode == _flexibleDates) ...[ + ShopInBitStep4Dropdown( + value: _selectedYear, + items: ["$currentYear", "${currentYear + 1}"], + hintText: "Year", + onChanged: (value) => setState(() => _selectedYear = value), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4Dropdown( + value: _selectedMonthSeason, + items: _months, + hintText: "Month or season", + onChanged: (value) => setState(() => _selectedMonthSeason = value), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4TextField( + controller: _tripLengthController, + focusNode: _tripLengthFocusNode, + hintText: "Number of nights", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + errorText: tripLengthError, + onChanged: (_) => setState(() {}), + ), + ], + + SizedBox(height: isDesktop ? 24 : 16), + _TravelSectionLabel(text: "Who", isDesktop: isDesktop), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitTravelerCounter( + label: "Adults", + value: _adults, + min: 1, + onChanged: (v) => setState(() => _adults = v), + ), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitTravelerCounter( + label: "Children", + value: _children, + onChanged: (v) => setState(() => _children = v), + ), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitTravelerCounter( + label: "Infants", + value: _infants, + onChanged: (v) => setState(() => _infants = v), + ), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitTravelerCounter( + label: "Pets", + value: _pets, + onChanged: (v) => setState(() => _pets = v), + ), + + SizedBox(height: isDesktop ? 24 : 16), + _TravelSectionLabel(text: "Budget", isDesktop: isDesktop), + SizedBox(height: isDesktop ? 12 : 8), + ShopInBitStep4TextField( + controller: _travelBudgetController, + focusNode: _travelBudgetFocusNode, + hintText: "Minimum 1000 EUR", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + suffixText: "EUR", + errorText: travelBudgetError, + onChanged: (_) => setState(() {}), + ), + + // Travel doesn't collect delivery country: destinations are in the + // form and the API field is set to "DE" on submit. + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitPrivacyCheckbox( + value: _privacyAccepted, + onChanged: (v) => setState(() => _privacyAccepted = v), + ), + SizedBox(height: isDesktop ? 16 : 12), + ShopInBitStep4SubmitButton( + submitting: _submitting, + enabled: _canContinue, + onPressed: _submit, + ), + ], + ); + } +} + +/// Bold-ish section header used inside the travel form ("Trip type", "Where", +/// "When", "Who", "Budget"). +class _TravelSectionLabel extends StatelessWidget { + const _TravelSectionLabel({required this.text, required this.isDesktop}); + + final String text; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return Text( + text, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w500_14(context), + ); + } +} diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_traveler_counter.dart b/lib/pages/shopinbit/step_4_components/shopinbit_traveler_counter.dart new file mode 100644 index 000000000..fb5ab6d41 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_traveler_counter.dart @@ -0,0 +1,85 @@ +import "package:flutter/material.dart"; + +import "../../../themes/stack_colors.dart"; +import "../../../utilities/constants.dart"; +import "../../../utilities/text_styles.dart"; +import "../../../utilities/util.dart"; + +/// Label + minus/value/plus counter row used in the travel form to set the +/// number of adults, children, infants and pets. +class ShopInBitTravelerCounter extends StatelessWidget { + const ShopInBitTravelerCounter({ + super.key, + required this.label, + required this.value, + required this.onChanged, + this.min = 0, + this.max = 20, + }); + + final String label; + final int value; + final int min; + final int max; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final TextStyle textStyle = Util.isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w500_14(context); + + return Row( + children: [ + Text(label, style: textStyle), + const Spacer(), + _CounterButton( + symbol: "-", + onTap: value > min ? () => onChanged(value - 1) : null, + textStyle: textStyle, + ), + const SizedBox(width: 16), + SizedBox( + width: 24, + child: Center(child: Text("$value", style: textStyle)), + ), + const SizedBox(width: 16), + _CounterButton( + symbol: "+", + onTap: value < max ? () => onChanged(value + 1) : null, + textStyle: textStyle, + ), + ], + ); + } +} + +class _CounterButton extends StatelessWidget { + const _CounterButton({ + required this.symbol, + required this.onTap, + required this.textStyle, + }); + + final String symbol; + final VoidCallback? onTap; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Center(child: Text(symbol, style: textStyle)), + ), + ); + } +} From a4d82ec1a896d83dc237b3bfdd6b61f7efae356e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 09:08:02 -0600 Subject: [PATCH 10/27] fix: This does not actually return a 403 when testing and is required to get a status update. Commenting out for now as otherwise the request will stay pending for ever in the UI --- lib/pages/shopinbit/shopinbit_tickets_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index d600a00fc..32b65ceeb 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -123,7 +123,7 @@ class _ShopInBitTicketsViewState extends State { if (localIdx < 0) continue; // Car research tickets return 403 on /tickets/:id/* endpoints. - if (_tickets[localIdx].category == ShopInBitCategory.car) continue; + // if (_tickets[localIdx].category == ShopInBitCategory.car) continue; final statusResp = await service.client.getTicketStatus(ref.id); if (statusResp.hasError || statusResp.value == null) continue; From c6b313bb943e2f32039348bd3b80914f7c029be5 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 09:09:48 -0600 Subject: [PATCH 11/27] fix: throw instead of silent failure leading to invalid enum value returns. And some other cleanup --- .../shopinbit/shopinbit_order_model.dart | 27 +++++++- .../shopinbit/shopinbit_ticket_detail.dart | 57 +++------------- .../shopinbit/shopinbit_tickets_view.dart | 65 ++++--------------- lib/services/shopinbit/src/models/ticket.dart | 14 ++-- .../shopinbit/src/models/webhook_event.dart | 7 +- 5 files changed, 57 insertions(+), 113 deletions(-) diff --git a/lib/models/shopinbit/shopinbit_order_model.dart b/lib/models/shopinbit/shopinbit_order_model.dart index f41aa49e3..88d247c79 100644 --- a/lib/models/shopinbit/shopinbit_order_model.dart +++ b/lib/models/shopinbit/shopinbit_order_model.dart @@ -1,6 +1,9 @@ +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import '../../services/shopinbit/src/models/ticket.dart'; +import '../../themes/stack_colors.dart'; import '../isar/models/shopinbit_ticket.dart'; enum ShopInBitCategory { concierge, travel, car } @@ -16,7 +19,29 @@ enum ShopInBitOrderStatus { delivered, closed, cancelled, - refunded, + refunded; + + String get label => switch (this) { + .pending => "Pending", + .reviewing => "Under review", + .offerAvailable => "Offer available", + .accepted => "Accepted", + .paymentPending => "Awaiting payment", + .paid => "Paid", + .shipping => "Shipping", + .delivered => "Delivered", + .closed => "Closed", + .cancelled => "Cancelled", + .refunded => "Refunded", + }; + + Color getColor(StackColors colors) => switch (this) { + .delivered => colors.accentColorGreen, + .offerAvailable => colors.accentColorBlue, + .pending || .reviewing => colors.accentColorYellow, + .closed || .cancelled || .refunded => colors.textSubtitle1, + _ => colors.accentColorDark, + }; } class ShopInBitMessage { diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 1a7781656..99e799fb0 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -33,51 +33,6 @@ class ShopInBitTicketDetail extends StatefulWidget { class _ShopInBitTicketDetailState extends State { late final TextEditingController _messageController; - String _statusLabel(ShopInBitOrderStatus status) { - switch (status) { - case ShopInBitOrderStatus.pending: - return "Pending"; - case ShopInBitOrderStatus.reviewing: - return "Under review"; - case ShopInBitOrderStatus.offerAvailable: - return "Offer available"; - case ShopInBitOrderStatus.accepted: - return "Accepted"; - case ShopInBitOrderStatus.paymentPending: - return "Awaiting payment"; - case ShopInBitOrderStatus.paid: - return "Paid"; - case ShopInBitOrderStatus.shipping: - return "Shipping"; - case ShopInBitOrderStatus.delivered: - return "Delivered"; - case ShopInBitOrderStatus.closed: - return "Closed"; - case ShopInBitOrderStatus.cancelled: - return "Cancelled"; - case ShopInBitOrderStatus.refunded: - return "Refunded"; - } - } - - Color _statusColor(BuildContext context, ShopInBitOrderStatus status) { - switch (status) { - case ShopInBitOrderStatus.delivered: - return Theme.of(context).extension()!.accentColorGreen; - case ShopInBitOrderStatus.offerAvailable: - return Theme.of(context).extension()!.accentColorBlue; - case ShopInBitOrderStatus.pending: - case ShopInBitOrderStatus.reviewing: - return Theme.of(context).extension()!.accentColorYellow; - case ShopInBitOrderStatus.closed: - case ShopInBitOrderStatus.cancelled: - case ShopInBitOrderStatus.refunded: - return Theme.of(context).extension()!.textSubtitle1; - default: - return Theme.of(context).extension()!.accentColorDark; - } - } - bool _sending = false; bool _loading = false; bool _retrying = false; @@ -425,15 +380,21 @@ class _ShopInBitTicketDetailState extends State { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: _statusColor(context, model.status).withOpacity(0.2), + color: model.status + .getColor(Theme.of(context).extension()!) + .withOpacity(0.2), ), child: Text( - _statusLabel(model.status), + model.status.label, style: (isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) : STextStyles.itemSubtitle12(context)) - .copyWith(color: _statusColor(context, model.status)), + .copyWith( + color: model.status.getColor( + Theme.of(context).extension()!, + ), + ), ), ), ], diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index 32b65ceeb..0ff73b681 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -172,51 +172,6 @@ class _ShopInBitTicketsViewState extends State { } } - String _statusLabel(ShopInBitOrderStatus status) { - switch (status) { - case ShopInBitOrderStatus.pending: - return "Pending"; - case ShopInBitOrderStatus.reviewing: - return "Under review"; - case ShopInBitOrderStatus.offerAvailable: - return "Offer available"; - case ShopInBitOrderStatus.accepted: - return "Accepted"; - case ShopInBitOrderStatus.paymentPending: - return "Awaiting payment"; - case ShopInBitOrderStatus.paid: - return "Paid"; - case ShopInBitOrderStatus.shipping: - return "Shipping"; - case ShopInBitOrderStatus.delivered: - return "Delivered"; - case ShopInBitOrderStatus.closed: - return "Closed"; - case ShopInBitOrderStatus.cancelled: - return "Cancelled"; - case ShopInBitOrderStatus.refunded: - return "Refunded"; - } - } - - Color _statusColor(BuildContext context, ShopInBitOrderStatus status) { - switch (status) { - case ShopInBitOrderStatus.delivered: - return Theme.of(context).extension()!.accentColorGreen; - case ShopInBitOrderStatus.offerAvailable: - return Theme.of(context).extension()!.accentColorBlue; - case ShopInBitOrderStatus.pending: - case ShopInBitOrderStatus.reviewing: - return Theme.of(context).extension()!.accentColorYellow; - case ShopInBitOrderStatus.closed: - case ShopInBitOrderStatus.cancelled: - case ShopInBitOrderStatus.refunded: - return Theme.of(context).extension()!.textSubtitle1; - default: - return Theme.of(context).extension()!.accentColorDark; - } - } - String _categoryLabel(ShopInBitCategory? category) { switch (category) { case ShopInBitCategory.concierge: @@ -359,13 +314,16 @@ class _ShopInBitTicketsViewState extends State { ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: _statusColor( - context, - ticket.status, - ).withOpacity(0.2), + color: ticket.status + .getColor( + Theme.of( + context, + ).extension()!, + ) + .withOpacity(0.2), ), child: Text( - _statusLabel(ticket.status), + ticket.status.label, style: (isDesktop ? STextStyles.desktopTextExtraExtraSmall( @@ -375,9 +333,10 @@ class _ShopInBitTicketsViewState extends State { context, )) .copyWith( - color: _statusColor( - context, - ticket.status, + color: ticket.status.getColor( + Theme.of( + context, + ).extension()!, ), ), ), diff --git a/lib/services/shopinbit/src/models/ticket.dart b/lib/services/shopinbit/src/models/ticket.dart index eec6dd360..2f8e91d06 100644 --- a/lib/services/shopinbit/src/models/ticket.dart +++ b/lib/services/shopinbit/src/models/ticket.dart @@ -16,10 +16,10 @@ enum TicketState { final String value; const TicketState(this.value); - static TicketState fromString(String s) { + static TicketState fromString(String value) { return TicketState.values.firstWhere( - (e) => e.value == s, - orElse: () => TicketState.newTicket, + (e) => e.value == value, + orElse: () => throw Exception("Unknown TicketState string found: $value"), ); } } @@ -104,9 +104,7 @@ class TicketFull { } } -int _toInt(dynamic v) { - if (v is int) return v; - if (v is String) return int.parse(v); - if (v is double) return v.toInt(); - return 0; +int _toInt(dynamic value) { + if (value is int) return value; + return int.parse(value.toString()); } diff --git a/lib/services/shopinbit/src/models/webhook_event.dart b/lib/services/shopinbit/src/models/webhook_event.dart index 7bf41694e..67a160b2c 100644 --- a/lib/services/shopinbit/src/models/webhook_event.dart +++ b/lib/services/shopinbit/src/models/webhook_event.dart @@ -5,10 +5,11 @@ enum WebhookEventType { final String value; const WebhookEventType(this.value); - static WebhookEventType fromString(String s) { + static WebhookEventType fromString(String value) { return WebhookEventType.values.firstWhere( - (e) => e.value == s, - orElse: () => WebhookEventType.ticketStateChanged, + (e) => e.value == value, + orElse: () => + throw Exception("Unknown WebhookEventType string found: $value"), ); } } From 7ddaa909f138ad588738200aa199bd1e0dace361 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 10:01:37 -0600 Subject: [PATCH 12/27] fix: allow shopinbit car request status updates --- .../shopinbit/shopinbit_ticket_detail.dart | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 99e799fb0..671ee89d0 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -68,44 +68,39 @@ class _ShopInBitTicketDetailState extends State { final client = ShopInBitService.instance.client; final id = widget.model.apiTicketId; - // Car research tickets created via /car-research/log-payment are not - // accessible via /tickets/:id/* endpoints (API returns 403). Skip - // those calls for car tickets to avoid log spam. Local data is used. - if (!_isCarResearch) { - final messagesResp = await client.getMessages(id); - final statusResp = await client.getTicketStatus(id); - - if (!messagesResp.hasError && messagesResp.value != null) { - final apiMessages = messagesResp.value!; - widget.model.clearMessages(); - for (final m in apiMessages) { - widget.model.addMessage( - ShopInBitMessage( - text: m.content, - timestamp: m.timestamp, - isFromUser: !m.fromAgent, - ), - ); - } - } - - if (!statusResp.hasError && statusResp.value != null) { - widget.model.status = ShopInBitOrderModel.statusFromTicketState( - statusResp.value!.state, + final messagesResp = await client.getMessages(id); + final statusResp = await client.getTicketStatus(id); + + if (!messagesResp.hasError && messagesResp.value != null) { + final apiMessages = messagesResp.value!; + widget.model.clearMessages(); + for (final m in apiMessages) { + widget.model.addMessage( + ShopInBitMessage( + text: m.content, + timestamp: m.timestamp, + isFromUser: !m.fromAgent, + ), ); } + } - if (widget.model.status == ShopInBitOrderStatus.offerAvailable && - (widget.model.offerProductName == null || - widget.model.offerPrice == null)) { - final offerResp = await client.getTicketFull(id); - if (!offerResp.hasError && offerResp.value != null) { - final t = offerResp.value!; - widget.model.setOffer( - productName: t.productName, - price: t.customerPrice, - ); - } + if (!statusResp.hasError && statusResp.value != null) { + widget.model.status = ShopInBitOrderModel.statusFromTicketState( + statusResp.value!.state, + ); + } + + if (widget.model.status == ShopInBitOrderStatus.offerAvailable && + (widget.model.offerProductName == null || + widget.model.offerPrice == null)) { + final offerResp = await client.getTicketFull(id); + if (!offerResp.hasError && offerResp.value != null) { + final t = offerResp.value!; + widget.model.setOffer( + productName: t.productName, + price: t.customerPrice, + ); } } From fa5fa8125a72913d20975f3167522f841b5bb14d Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 16:38:38 -0600 Subject: [PATCH 13/27] refactor(shopinbit): Store shop in bit settings using Drift, use providers for drift shared db and shopinbit service, and some general clean up and tweaks --- lib/db/drift/shared_database.dart | 63 +- lib/db/drift/shared_database.g.dart | 544 +++++++++- lib/pages/more_view/services_view.dart | 25 +- .../shopinbit/shopinbit_car_fee_view.dart | 39 +- .../shopinbit_car_research_payment_view.dart | 80 +- lib/pages/shopinbit/shopinbit_offer_view.dart | 16 +- .../shopinbit/shopinbit_payment_view.dart | 31 +- .../shopinbit/shopinbit_send_from_view.dart | 6 +- .../shopinbit/shopinbit_settings_view.dart | 996 ++++++++++++------ lib/pages/shopinbit/shopinbit_setup_view.dart | 74 +- .../shopinbit/shopinbit_shipping_view.dart | 39 +- lib/pages/shopinbit/shopinbit_step_2.dart | 20 +- lib/pages/shopinbit/shopinbit_step_3.dart | 11 +- .../shopinbit/shopinbit_ticket_detail.dart | 39 +- .../shopinbit/shopinbit_tickets_view.dart | 12 +- .../shopinbit_concierge_form.dart | 16 +- .../shopinbit_country_picker.dart | 13 +- .../shopinbit_generic_form.dart | 15 +- .../shopinbit_step4_submit.dart | 2 +- .../shopinbit_travel_form.dart | 15 +- .../shopin_bit/desktop_shopinbit_view.dart | 50 +- .../desktop_shopin_bit_first_run.dart | 76 +- .../settings/desktop_settings_view.dart | 4 +- .../settings_menu/shopinbit_settings.dart | 550 ---------- lib/providers/db/drift_provider.dart | 5 +- .../global/shopin_bit_service_provider.dart | 8 + lib/route_generator.dart | 8 - lib/services/shopinbit/shopinbit_service.dart | 164 +-- .../paynym/paynym_is_api_test.mocks.dart | 53 + 29 files changed, 1708 insertions(+), 1266 deletions(-) delete mode 100644 lib/pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart create mode 100644 lib/providers/global/shopin_bit_service_provider.dart diff --git a/lib/db/drift/shared_database.dart b/lib/db/drift/shared_database.dart index 2e00d3c90..e83028d42 100644 --- a/lib/db/drift/shared_database.dart +++ b/lib/db/drift/shared_database.dart @@ -28,13 +28,72 @@ class CakepayOrders extends Table { Set get primaryKey => {orderId}; } -@DriftDatabase(tables: [CakepayOrders]) +class ShopinBitSettings extends Table { + // Single row table - always row 0 + IntColumn get id => integer().withDefault(const Constant(0))(); + + BoolColumn get guidelinesAccepted => + boolean().withDefault(const Constant(false))(); + BoolColumn get setupComplete => + boolean().withDefault(const Constant(false))(); + TextColumn get displayName => text().nullable()(); + + @override + Set get primaryKey => {id}; +} + +@DriftAccessor(tables: [ShopinBitSettings]) +class ShopinBitSettingsDao extends DatabaseAccessor + with _$ShopinBitSettingsDaoMixin { + ShopinBitSettingsDao(super.db); + + Future getSettings() async { + final ShopinBitSetting? row = await (select( + shopinBitSettings, + )..where((t) => t.id.equals(0))).getSingleOrNull(); + if (row != null) return row; + + return into( + shopinBitSettings, + ).insertReturning(ShopinBitSettingsCompanion.insert(id: const Value(0))); + } + + Future setGuidelinesAccepted(bool accepted) => + _update(ShopinBitSettingsCompanion(guidelinesAccepted: Value(accepted))); + + Future setSetupComplete(bool complete) => + _update(ShopinBitSettingsCompanion(setupComplete: Value(complete))); + + Future setDisplayName(String name) => + _update(ShopinBitSettingsCompanion(displayName: Value(name))); + + Future _update(ShopinBitSettingsCompanion changes) async { + await getSettings(); // ensure row exists + await (update( + shopinBitSettings, + )..where((t) => t.id.equals(0))).write(changes); + } +} + +@DriftDatabase( + tables: [CakepayOrders, ShopinBitSettings], + daos: [ShopinBitSettingsDao], +) final class SharedDatabase extends _$SharedDatabase { SharedDatabase._([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from == 1 && to == 2) { + await m.createTable(shopinBitSettings); + } + }, + ); static QueryExecutor _openConnection() { return driftDatabase( diff --git a/lib/db/drift/shared_database.g.dart b/lib/db/drift/shared_database.g.dart index 2e0c5de8b..9b7d8d7a3 100644 --- a/lib/db/drift/shared_database.g.dart +++ b/lib/db/drift/shared_database.g.dart @@ -3,6 +3,22 @@ part of 'shared_database.dart'; // ignore_for_file: type=lint +mixin _$ShopinBitSettingsDaoMixin on DatabaseAccessor { + $ShopinBitSettingsTable get shopinBitSettings => + attachedDatabase.shopinBitSettings; + ShopinBitSettingsDaoManager get managers => ShopinBitSettingsDaoManager(this); +} + +class ShopinBitSettingsDaoManager { + final _$ShopinBitSettingsDaoMixin _db; + ShopinBitSettingsDaoManager(this._db); + $$ShopinBitSettingsTableTableManager get shopinBitSettings => + $$ShopinBitSettingsTableTableManager( + _db.attachedDatabase, + _db.shopinBitSettings, + ); +} + class $CakepayOrdersTable extends CakepayOrders with TableInfo<$CakepayOrdersTable, CakepayOrder> { @override @@ -165,15 +181,345 @@ class CakepayOrdersCompanion extends UpdateCompanion { } } +class $ShopinBitSettingsTable extends ShopinBitSettings + with TableInfo<$ShopinBitSettingsTable, ShopinBitSetting> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShopinBitSettingsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _guidelinesAcceptedMeta = + const VerificationMeta('guidelinesAccepted'); + @override + late final GeneratedColumn guidelinesAccepted = GeneratedColumn( + 'guidelines_accepted', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("guidelines_accepted" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _setupCompleteMeta = const VerificationMeta( + 'setupComplete', + ); + @override + late final GeneratedColumn setupComplete = GeneratedColumn( + 'setup_complete', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("setup_complete" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + guidelinesAccepted, + setupComplete, + displayName, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shopin_bit_settings'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('guidelines_accepted')) { + context.handle( + _guidelinesAcceptedMeta, + guidelinesAccepted.isAcceptableOrUnknown( + data['guidelines_accepted']!, + _guidelinesAcceptedMeta, + ), + ); + } + if (data.containsKey('setup_complete')) { + context.handle( + _setupCompleteMeta, + setupComplete.isAcceptableOrUnknown( + data['setup_complete']!, + _setupCompleteMeta, + ), + ); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ShopinBitSetting map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShopinBitSetting( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + guidelinesAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}guidelines_accepted'], + )!, + setupComplete: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}setup_complete'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + ), + ); + } + + @override + $ShopinBitSettingsTable createAlias(String alias) { + return $ShopinBitSettingsTable(attachedDatabase, alias); + } +} + +class ShopinBitSetting extends DataClass + implements Insertable { + final int id; + final bool guidelinesAccepted; + final bool setupComplete; + final String? displayName; + const ShopinBitSetting({ + required this.id, + required this.guidelinesAccepted, + required this.setupComplete, + this.displayName, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['guidelines_accepted'] = Variable(guidelinesAccepted); + map['setup_complete'] = Variable(setupComplete); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + return map; + } + + ShopinBitSettingsCompanion toCompanion(bool nullToAbsent) { + return ShopinBitSettingsCompanion( + id: Value(id), + guidelinesAccepted: Value(guidelinesAccepted), + setupComplete: Value(setupComplete), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + ); + } + + factory ShopinBitSetting.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShopinBitSetting( + id: serializer.fromJson(json['id']), + guidelinesAccepted: serializer.fromJson(json['guidelinesAccepted']), + setupComplete: serializer.fromJson(json['setupComplete']), + displayName: serializer.fromJson(json['displayName']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'guidelinesAccepted': serializer.toJson(guidelinesAccepted), + 'setupComplete': serializer.toJson(setupComplete), + 'displayName': serializer.toJson(displayName), + }; + } + + ShopinBitSetting copyWith({ + int? id, + bool? guidelinesAccepted, + bool? setupComplete, + Value displayName = const Value.absent(), + }) => ShopinBitSetting( + id: id ?? this.id, + guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, + setupComplete: setupComplete ?? this.setupComplete, + displayName: displayName.present ? displayName.value : this.displayName, + ); + ShopinBitSetting copyWithCompanion(ShopinBitSettingsCompanion data) { + return ShopinBitSetting( + id: data.id.present ? data.id.value : this.id, + guidelinesAccepted: data.guidelinesAccepted.present + ? data.guidelinesAccepted.value + : this.guidelinesAccepted, + setupComplete: data.setupComplete.present + ? data.setupComplete.value + : this.setupComplete, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + ); + } + + @override + String toString() { + return (StringBuffer('ShopinBitSetting(') + ..write('id: $id, ') + ..write('guidelinesAccepted: $guidelinesAccepted, ') + ..write('setupComplete: $setupComplete, ') + ..write('displayName: $displayName') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, guidelinesAccepted, setupComplete, displayName); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShopinBitSetting && + other.id == this.id && + other.guidelinesAccepted == this.guidelinesAccepted && + other.setupComplete == this.setupComplete && + other.displayName == this.displayName); +} + +class ShopinBitSettingsCompanion extends UpdateCompanion { + final Value id; + final Value guidelinesAccepted; + final Value setupComplete; + final Value displayName; + const ShopinBitSettingsCompanion({ + this.id = const Value.absent(), + this.guidelinesAccepted = const Value.absent(), + this.setupComplete = const Value.absent(), + this.displayName = const Value.absent(), + }); + ShopinBitSettingsCompanion.insert({ + this.id = const Value.absent(), + this.guidelinesAccepted = const Value.absent(), + this.setupComplete = const Value.absent(), + this.displayName = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? guidelinesAccepted, + Expression? setupComplete, + Expression? displayName, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (guidelinesAccepted != null) 'guidelines_accepted': guidelinesAccepted, + if (setupComplete != null) 'setup_complete': setupComplete, + if (displayName != null) 'display_name': displayName, + }); + } + + ShopinBitSettingsCompanion copyWith({ + Value? id, + Value? guidelinesAccepted, + Value? setupComplete, + Value? displayName, + }) { + return ShopinBitSettingsCompanion( + id: id ?? this.id, + guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, + setupComplete: setupComplete ?? this.setupComplete, + displayName: displayName ?? this.displayName, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (guidelinesAccepted.present) { + map['guidelines_accepted'] = Variable(guidelinesAccepted.value); + } + if (setupComplete.present) { + map['setup_complete'] = Variable(setupComplete.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShopinBitSettingsCompanion(') + ..write('id: $id, ') + ..write('guidelinesAccepted: $guidelinesAccepted, ') + ..write('setupComplete: $setupComplete, ') + ..write('displayName: $displayName') + ..write(')')) + .toString(); + } +} + abstract class _$SharedDatabase extends GeneratedDatabase { _$SharedDatabase(QueryExecutor e) : super(e); $SharedDatabaseManager get managers => $SharedDatabaseManager(this); late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this); + late final $ShopinBitSettingsTable shopinBitSettings = + $ShopinBitSettingsTable(this); + late final ShopinBitSettingsDao shopinBitSettingsDao = ShopinBitSettingsDao( + this as SharedDatabase, + ); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [cakepayOrders]; + List get allSchemaEntities => [ + cakepayOrders, + shopinBitSettings, + ]; } typedef $$CakepayOrdersTableCreateCompanionBuilder = @@ -294,10 +640,206 @@ typedef $$CakepayOrdersTableProcessedTableManager = CakepayOrder, PrefetchHooks Function() >; +typedef $$ShopinBitSettingsTableCreateCompanionBuilder = + ShopinBitSettingsCompanion Function({ + Value id, + Value guidelinesAccepted, + Value setupComplete, + Value displayName, + }); +typedef $$ShopinBitSettingsTableUpdateCompanionBuilder = + ShopinBitSettingsCompanion Function({ + Value id, + Value guidelinesAccepted, + Value setupComplete, + Value displayName, + }); + +class $$ShopinBitSettingsTableFilterComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ShopinBitSettingsTableOrderingComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ShopinBitSettingsTableAnnotationComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => column, + ); + + GeneratedColumn get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => column, + ); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); +} + +class $$ShopinBitSettingsTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting, + $$ShopinBitSettingsTableFilterComposer, + $$ShopinBitSettingsTableOrderingComposer, + $$ShopinBitSettingsTableAnnotationComposer, + $$ShopinBitSettingsTableCreateCompanionBuilder, + $$ShopinBitSettingsTableUpdateCompanionBuilder, + ( + ShopinBitSetting, + BaseReferences< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting + >, + ), + ShopinBitSetting, + PrefetchHooks Function() + > { + $$ShopinBitSettingsTableTableManager( + _$SharedDatabase db, + $ShopinBitSettingsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShopinBitSettingsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShopinBitSettingsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShopinBitSettingsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value guidelinesAccepted = const Value.absent(), + Value setupComplete = const Value.absent(), + Value displayName = const Value.absent(), + }) => ShopinBitSettingsCompanion( + id: id, + guidelinesAccepted: guidelinesAccepted, + setupComplete: setupComplete, + displayName: displayName, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + Value guidelinesAccepted = const Value.absent(), + Value setupComplete = const Value.absent(), + Value displayName = const Value.absent(), + }) => ShopinBitSettingsCompanion.insert( + id: id, + guidelinesAccepted: guidelinesAccepted, + setupComplete: setupComplete, + displayName: displayName, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ShopinBitSettingsTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting, + $$ShopinBitSettingsTableFilterComposer, + $$ShopinBitSettingsTableOrderingComposer, + $$ShopinBitSettingsTableAnnotationComposer, + $$ShopinBitSettingsTableCreateCompanionBuilder, + $$ShopinBitSettingsTableUpdateCompanionBuilder, + ( + ShopinBitSetting, + BaseReferences< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting + >, + ), + ShopinBitSetting, + PrefetchHooks Function() + >; class $SharedDatabaseManager { final _$SharedDatabase _db; $SharedDatabaseManager(this._db); $$CakepayOrdersTableTableManager get cakepayOrders => $$CakepayOrdersTableTableManager(_db, _db.cakepayOrders); + $$ShopinBitSettingsTableTableManager get shopinBitSettings => + $$ShopinBitSettingsTableTableManager(_db, _db.shopinBitSettings); } diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart index aa4d7acda..210c45458 100644 --- a/lib/pages/more_view/services_view.dart +++ b/lib/pages/more_view/services_view.dart @@ -1,10 +1,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; @@ -14,23 +16,21 @@ 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 { +class ServicesView extends ConsumerStatefulWidget { const ServicesView({super.key}); static const String routeName = "/servicesView"; @override - State createState() => _ServicesViewState(); + ConsumerState createState() => _ServicesViewState(); } -class _ServicesViewState extends State { +class _ServicesViewState extends ConsumerState { Future _showOpenBrowserWarning(BuildContext context, String url) async { final uri = Uri.parse(url); final shouldContinue = await showDialog( @@ -69,7 +69,7 @@ class _ServicesViewState extends State { return shouldContinue ?? false; } - void _showShopDialog(BuildContext context) { + void _showShopDialog() { showDialog( context: context, barrierDismissible: true, @@ -142,12 +142,17 @@ class _ServicesViewState extends State { onPressed: () async { Navigator.of(dialogContext).pop(); final model = ShopInBitOrderModel(); - final service = ShopInBitService.instance; + final settings = await ref + .read(pSharedDrift) + .shopinBitSettingsDao + .getSettings(); - if (service.loadSetupComplete()) { + if (!mounted) return; + + if (settings.setupComplete) { // Returning user: pre-load display name, // skip Step 1, go to Step 2 - final savedName = service.loadDisplayName(); + final savedName = settings.displayName; if (savedName != null && savedName.isNotEmpty) { model.displayName = savedName; } @@ -303,7 +308,7 @@ class _ServicesViewState extends State { PrimaryButton( label: "Shop with ShopinBit", enabled: true, - onPressed: () => _showShopDialog(context), + onPressed: _showShopDialog, ), const SizedBox(height: 12), Builder( diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart index 7f691375d..4f88893a7 100644 --- a/lib/pages/shopinbit/shopinbit_car_fee_view.dart +++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart @@ -3,12 +3,13 @@ import 'dart:convert'; 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 '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../services/shopinbit/src/models/address.dart'; import '../../services/shopinbit/src/models/car_research.dart'; import '../../themes/stack_colors.dart'; @@ -17,7 +18,6 @@ import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; -import '../more_view/services_view.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog.dart'; @@ -25,10 +25,11 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_text_field.dart'; +import '../more_view/services_view.dart'; import 'shopinbit_car_research_payment_view.dart'; import 'shopinbit_step_2.dart'; -class ShopInBitCarFeeView extends StatefulWidget { +class ShopInBitCarFeeView extends ConsumerStatefulWidget { const ShopInBitCarFeeView({super.key, required this.model}); static const String routeName = "/shopInBitCarFee"; @@ -36,10 +37,11 @@ class ShopInBitCarFeeView extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitCarFeeViewState(); + ConsumerState createState() => + _ShopInBitCarFeeViewState(); } -class _ShopInBitCarFeeViewState extends State { +class _ShopInBitCarFeeViewState extends ConsumerState { late final TextEditingController _nameController; late final TextEditingController _streetController; late final TextEditingController _cityController; @@ -179,7 +181,7 @@ class _ShopInBitCarFeeViewState extends State { Future _fetchCountries() async { setState(() => _loadingCountries = true); try { - final resp = await ShopInBitService.instance.client.getCountries(); + final resp = await ref.read(pShopinBitService).client.getCountries(); if (resp.hasError || resp.value == null) return; _countries = resp.value!; if (_selectedCountryIso != null && @@ -209,7 +211,7 @@ class _ShopInBitCarFeeViewState extends State { if (_submitting) return; setState(() => _submitting = true); try { - await ShopInBitService.instance.ensureCustomerKey(); + await ref.read(pShopinBitService).ensureCustomerKey(); // Delivery address (always provided) final deliveryName = _splitFullName(_nameController.text); @@ -221,7 +223,8 @@ class _ShopInBitCarFeeViewState extends State { country: _selectedCountryIso!, ); - // Billing address: use separate billing fields if different, else use delivery + // Billing address: use separate billing fields if different, + // else use delivery final Address billing; if (_differentBilling) { final billingName = _splitFullName(_billingNameController.text); @@ -244,7 +247,9 @@ class _ShopInBitCarFeeViewState extends State { ); } - final resp = await ShopInBitService.instance.client + final resp = await ref + .read(pShopinBitService) + .client .createCarResearchInvoice(billing: billing); if (resp.hasError || resp.value == null) { @@ -264,7 +269,8 @@ class _ShopInBitCarFeeViewState extends State { final invoice = resp.value!; // Persist pending state so the user can resume if they close the dialog. - // Sentinel ticketId; unique-replace index ensures at most one pending record. + // Sentinel ticketId; unique-replace index ensures at most one pending + // record. widget.model.ticketId = "pending-car-research"; widget.model.carResearchInvoiceId = invoice.btcpayInvoice; widget.model.isPendingPayment = true; @@ -328,7 +334,9 @@ class _ShopInBitCarFeeViewState extends State { // a fee field. Today the endpoint returns only {status, additional}, so // we source the displayed amount from the BIP21 payment URIs instead. try { - final resp = await ShopInBitService.instance.client + final resp = await ref + .read(pShopinBitService) + .client .getCarResearchInvoiceStatus(invoice.btcpayInvoice); if (resp.hasError || resp.value == null) { Logging.instance.i( @@ -471,9 +479,12 @@ class _ShopInBitCarFeeViewState extends State { Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of( - context, - ).extension()!.textFieldActiveSearchIconRight, + colorFilter: ColorFilter.mode( + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + .srcIn, + ), ), ), ), diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart index 0073ee831..38db1c73b 100644 --- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart @@ -10,9 +10,9 @@ import '../../db/isar/main_db.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; import '../../services/shopinbit/src/models/car_research.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/address_utils.dart'; @@ -22,16 +22,16 @@ import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; -import '../more_view/services_view.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; -import '../../widgets/stack_dialog.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../more_view/services_view.dart'; import 'shopinbit_order_created.dart'; import 'shopinbit_send_from_view.dart'; import 'shopinbit_tickets_view.dart'; @@ -240,7 +240,8 @@ class _ShopInBitCarResearchPaymentViewState showFloatingFlushBar( type: FlushBarType.info, message: - "Payment not yet confirmed. Please wait a moment and try again.", + "Payment not yet confirmed. " + "Please wait a moment and try again.", context: context, ), ); @@ -345,7 +346,9 @@ class _ShopInBitCarResearchPaymentViewState Future _pollStatus() async { try { - final resp = await ShopInBitService.instance.client + final resp = await ref + .read(pShopinBitService) + .client .getCarResearchInvoiceStatus(widget.invoice.btcpayInvoice); if (resp.hasError || resp.value == null) { if (mounted) { @@ -394,8 +397,9 @@ class _ShopInBitCarResearchPaymentViewState if (_flowState == _PaymentFlowState.loggingPayment || _flowState == _PaymentFlowState.creatingRequest || _flowState == _PaymentFlowState.complete || - _flowState == _PaymentFlowState.error) + _flowState == _PaymentFlowState.error) { return; + } // Skip logCarResearchPayment if the fee was already logged. final existingFeeTicket = widget.model.feeTicketNumber; @@ -426,17 +430,22 @@ class _ShopInBitCarResearchPaymentViewState setState(() => _flowState = _PaymentFlowState.creatingRequest); _pollTimer?.cancel(); try { - final customerKey = await ShopInBitService.instance.ensureCustomerKey(); + final customerKey = await ref + .read(pShopinBitService) + .ensureCustomerKey(); final comment = "${widget.model.requestDescription}\n\n" "The Client paid the car research fee (#$existingFeeTicket)"; - final reqResp = await ShopInBitService.instance.client.createRequest( - customerPseudonym: widget.model.displayName, - externalCustomerKey: customerKey, - serviceType: "car", - comment: comment, - deliveryCountry: widget.model.deliveryCountry, - ); + final reqResp = await ref + .read(pShopinBitService) + .client + .createRequest( + customerPseudonym: widget.model.displayName, + externalCustomerKey: customerKey, + serviceType: "car", + comment: comment, + deliveryCountry: widget.model.deliveryCountry, + ); if (reqResp.hasError || reqResp.value == null) { if (mounted) { setState(() => _flowState = _PaymentFlowState.error); @@ -517,7 +526,9 @@ class _ShopInBitCarResearchPaymentViewState _pollTimer?.cancel(); try { - final logResp = await ShopInBitService.instance.client + final logResp = await ref + .read(pShopinBitService) + .client .logCarResearchPayment(widget.invoice.btcpayInvoice); if (logResp.hasError || logResp.value == null) { if (mounted) { @@ -535,7 +546,8 @@ class _ShopInBitCarResearchPaymentViewState final feeResult = logResp.value!; - // Persist feeTicketNumber on the existing model (a new DB row creates a spurious list entry). + // Persist feeTicketNumber on the existing model (a new DB row creates a + // spurious list entry). widget.model.feeTicketNumber = feeResult.ticketNumber; widget.model.needsCreateRequest = true; await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); @@ -543,18 +555,21 @@ class _ShopInBitCarResearchPaymentViewState if (!mounted) return; setState(() => _flowState = _PaymentFlowState.creatingRequest); - final customerKey = await ShopInBitService.instance.ensureCustomerKey(); + final customerKey = await ref.read(pShopinBitService).ensureCustomerKey(); final comment = "${widget.model.requestDescription}\n\n" "The Client paid the car research fee (#${feeResult.ticketNumber})"; - final reqResp = await ShopInBitService.instance.client.createRequest( - customerPseudonym: widget.model.displayName, - externalCustomerKey: customerKey, - serviceType: "car", - comment: comment, - deliveryCountry: widget.model.deliveryCountry, - ); + final reqResp = await ref + .read(pShopinBitService) + .client + .createRequest( + customerPseudonym: widget.model.displayName, + externalCustomerKey: customerKey, + serviceType: "car", + comment: comment, + deliveryCountry: widget.model.deliveryCountry, + ); if (reqResp.hasError || reqResp.value == null) { // createRequest failed: fee receipt already persisted, show retry @@ -645,13 +660,16 @@ class _ShopInBitCarResearchPaymentViewState "${widget.model.requestDescription}\n\n" "The Client paid the car research fee (#$feeTicketNumber)"; - final reqResp = await ShopInBitService.instance.client.createRequest( - customerPseudonym: widget.model.displayName, - externalCustomerKey: customerKey, - serviceType: "car", - comment: comment, - deliveryCountry: widget.model.deliveryCountry, - ); + final reqResp = await ref + .read(pShopinBitService) + .client + .createRequest( + customerPseudonym: widget.model.displayName, + externalCustomerKey: customerKey, + serviceType: "car", + comment: comment, + deliveryCountry: widget.model.deliveryCountry, + ); if (reqResp.hasError || reqResp.value == null) { if (mounted) { diff --git a/lib/pages/shopinbit/shopinbit_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart index f746b03c8..98946c14d 100644 --- a/lib/pages/shopinbit/shopinbit_offer_view.dart +++ b/lib/pages/shopinbit/shopinbit_offer_view.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; @@ -15,7 +16,7 @@ import '../../widgets/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_shipping_view.dart'; -class ShopInBitOfferView extends StatefulWidget { +class ShopInBitOfferView extends ConsumerStatefulWidget { const ShopInBitOfferView({super.key, required this.model}); static const String routeName = "/shopInBitOffer"; @@ -23,10 +24,10 @@ class ShopInBitOfferView extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitOfferViewState(); + ConsumerState createState() => _ShopInBitOfferViewState(); } -class _ShopInBitOfferViewState extends State { +class _ShopInBitOfferViewState extends ConsumerState { bool _loading = false; @override @@ -40,9 +41,10 @@ class _ShopInBitOfferViewState extends State { Future _loadOffer() async { setState(() => _loading = true); try { - final resp = await ShopInBitService.instance.client.getTicketFull( - widget.model.apiTicketId, - ); + final resp = await ref + .read(pShopinBitService) + .client + .getTicketFull(widget.model.apiTicketId); if (!resp.hasError && resp.value != null) { final t = resp.value!; widget.model.setOffer( diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart index 98136696d..eea1f437c 100644 --- a/lib/pages/shopinbit/shopinbit_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_payment_view.dart @@ -11,9 +11,9 @@ import '../../app_config.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; import '../../services/shopinbit/src/models/payment.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; @@ -108,9 +108,10 @@ class _ShopInBitPaymentViewState extends ConsumerState { Future _pollPayment() async { try { - final resp = await ShopInBitService.instance.client.getPayment( - widget.model.apiTicketId, - ); + final resp = await ref + .read(pShopinBitService) + .client + .getPayment(widget.model.apiTicketId); if (!resp.hasError && resp.value != null && mounted) { setState(() => _applyPaymentInfo(resp.value!)); if (_isTerminal) { @@ -123,9 +124,10 @@ class _ShopInBitPaymentViewState extends ConsumerState { Future _loadPayment() async { setState(() => _loading = true); try { - final resp = await ShopInBitService.instance.client.getPayment( - widget.model.apiTicketId, - ); + final resp = await ref + .read(pShopinBitService) + .client + .getPayment(widget.model.apiTicketId); if (!resp.hasError && resp.value != null) { _applyPaymentInfo(resp.value!); } @@ -142,10 +144,10 @@ class _ShopInBitPaymentViewState extends ConsumerState { Future _refreshInvoice() async { setState(() => _loading = true); try { - final resp = await ShopInBitService.instance.client.getPayment( - widget.model.apiTicketId, - retry: true, - ); + final resp = await ref + .read(pShopinBitService) + .client + .getPayment(widget.model.apiTicketId, retry: true); if (!resp.hasError && resp.value != null) { _applyPaymentInfo(resp.value!); } @@ -160,9 +162,10 @@ class _ShopInBitPaymentViewState extends ConsumerState { _pollTimer?.cancel(); setState(() => _loading = true); try { - final resp = await ShopInBitService.instance.client.getPayment( - widget.model.apiTicketId, - ); + final resp = await ref + .read(pShopinBitService) + .client + .getPayment(widget.model.apiTicketId); if (!resp.hasError && resp.value != null && mounted) { setState(() => _applyPaymentInfo(resp.value!)); final status = resp.value!.status; diff --git a/lib/pages/shopinbit/shopinbit_send_from_view.dart b/lib/pages/shopinbit/shopinbit_send_from_view.dart index 0060cf596..d2c08e26b 100644 --- a/lib/pages/shopinbit/shopinbit_send_from_view.dart +++ b/lib/pages/shopinbit/shopinbit_send_from_view.dart @@ -9,6 +9,8 @@ import '../../app_config.dart'; import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; +import '../../pages_desktop_specific/desktop_home_view.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/coin_icon_provider.dart'; @@ -25,7 +27,6 @@ import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/eth/token_balance_provider.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; import '../../wallets/wallet/impl/ethereum_wallet.dart'; import '../../wallets/wallet/intermediate/external_wallet.dart'; import '../../wallets/wallet/wallet.dart'; @@ -36,7 +37,6 @@ import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; -import '../../pages_desktop_specific/desktop_home_view.dart'; import '../home_view/home_view.dart'; import '../send_view/sub_widgets/building_transaction_dialog.dart'; import 'shopinbit_confirm_send_view.dart'; @@ -250,7 +250,7 @@ class _ShopInBitSendFromCardState extends ConsumerState { Amount? sendAmount = amount; if (sendAmount == null) { - if (ShopInBitService.instance.client.sandbox) { + if (ref.read(pShopinBitService).client.sandbox) { sendAmount = Amount( rawValue: BigInt.from(10000), fractionDigits: fractionDigits, diff --git a/lib/pages/shopinbit/shopinbit_settings_view.dart b/lib/pages/shopinbit/shopinbit_settings_view.dart index 0682b74e6..50b62043d 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -6,18 +6,22 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; -import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/rounded_container.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'; class ShopInBitSettingsView extends ConsumerStatefulWidget { const ShopInBitSettingsView({super.key}); @@ -31,11 +35,8 @@ class ShopInBitSettingsView extends ConsumerStatefulWidget { class _ShopInBitSettingsViewState extends ConsumerState { final _manualKeyController = TextEditingController(); - final _manualKeyFocusNode = FocusNode(); final _verifyKeyController = TextEditingController(); - final _verifyKeyFocusNode = FocusNode(); - late final TextEditingController _displayNameController; - late final FocusNode _displayNameFocusNode; + final _displayNameController = TextEditingController(); String? _currentKey; bool _loading = false; @@ -44,20 +45,29 @@ class _ShopInBitSettingsViewState extends ConsumerState { @override void initState() { super.initState(); - _currentKey = ShopInBitService.instance.loadCustomerKey(); - final savedName = ShopInBitService.instance.loadDisplayName(); - _displayNameController = TextEditingController(text: savedName ?? ''); - _displayNameFocusNode = FocusNode(); + + // not the greatest solution but its the least invasive with the current + // ui code impl + () async { + final settings = await ref + .read(pSharedDrift) + .shopinBitSettingsDao + .getSettings(); + final key = await ref.read(pShopinBitService).loadCustomerKey(); + if (mounted) { + setState(() { + _currentKey = key; + _displayNameController.text = settings.displayName ?? ""; + }); + } + }(); } @override void dispose() { _manualKeyController.dispose(); - _manualKeyFocusNode.dispose(); _verifyKeyController.dispose(); - _verifyKeyFocusNode.dispose(); _displayNameController.dispose(); - _displayNameFocusNode.dispose(); super.dispose(); } @@ -66,7 +76,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { if (name.isEmpty) return; setState(() => _savingName = true); try { - await ShopInBitService.instance.setDisplayName(name); + await ref.read(pSharedDrift).shopinBitSettingsDao.setDisplayName(name); if (mounted) { unawaited( showFloatingFlushBar( @@ -91,11 +101,11 @@ class _ShopInBitSettingsViewState extends ConsumerState { try { final String key; if (_currentKey != null) { - final resp = await ShopInBitService.instance.client.generateKey(); + final resp = await ref.read(pShopinBitService).client.generateKey(); key = resp.valueOrThrow; - await ShopInBitService.instance.setCustomerKey(key); + await ref.read(pShopinBitService).setCustomerKey(key); } else { - key = await ShopInBitService.instance.ensureCustomerKey(); + key = await ref.read(pShopinBitService).ensureCustomerKey(); } setState(() => _currentKey = key); if (mounted) { @@ -133,7 +143,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { setState(() => _loading = true); try { - await ShopInBitService.instance.setCustomerKey(newKey); + await ref.read(pShopinBitService).setCustomerKey(newKey); setState(() { _currentKey = newKey; _manualKeyController.clear(); @@ -165,79 +175,165 @@ class _ShopInBitSettingsViewState extends ConsumerState { Future _showChangeWarning() async { final result = await showDialog( context: context, - barrierDismissible: true, - builder: (context) => StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Save your current key", - style: STextStyles.pageTitleH2(context), - ), - const SizedBox(height: 8), - SelectableText( - "Your current customer key is:", - style: STextStyles.smallMed14(context), - ), - const SizedBox(height: 8), - RoundedContainer( - color: Theme.of( - context, - ).extension()!.warningBackground, - child: SelectableText( - _currentKey!, - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of( - context, - ).extension()!.warningForeground, - ), - ), - ), - const SizedBox(height: 8), - SelectableText( - "Changing your key will disconnect you from " - "existing ShopinBit conversations. Make sure " - "you have saved your current key before " - "proceeding.", - style: STextStyles.smallMed14(context), - ), - const SizedBox(height: 20), - Row( + builder: (context) { + // TODO: this conditional can probably be merged when we have time + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 550, + maxHeight: double.infinity, + child: Column( children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () => Navigator.of(context).pop(false), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Save your current key", + style: STextStyles.desktopH3(context), ), ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Your current customer key is:", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox(height: 8), + RoundedWhiteContainer( + borderColor: Theme.of( + context, + ).extension()!.textSubtitle6, + child: SelectableText( + _currentKey!, + style: STextStyles.desktopTextSmall(context), + ), + ), + const SizedBox(height: 16), + Text( + "Changing your key will disconnect you from " + "existing ShopinBit requests. Make sure " + "you have saved your current key before " + "proceeding.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "I saved my key", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(null), + ), + ), + ], + ), + ], ), ), - const SizedBox(width: 8), - Expanded( - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () => Navigator.of(context).pop(null), - child: Text( - "I saved my key", - style: STextStyles.button(context), + ], + ), + ); + } else { + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Save your current key", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 8), + SelectableText( + "Your current customer key is:", + style: STextStyles.smallMed14(context), + ), + const SizedBox(height: 8), + RoundedContainer( + color: Theme.of( + context, + ).extension()!.warningBackground, + child: SelectableText( + _currentKey!, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of( + context, + ).extension()!.warningForeground, ), ), ), + const SizedBox(height: 8), + SelectableText( + "Changing your key will disconnect you from " + "existing ShopinBit conversations. Make sure " + "you have saved your current key before " + "proceeding.", + style: STextStyles.smallMed14(context), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + onPressed: () => Navigator.of(context).pop(false), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () => Navigator.of(context).pop(null), + child: Text( + "I saved my key", + style: STextStyles.button(context), + ), + ), + ), + ], + ), ], ), - ], - ), - ), + ); + } + }, ); if (result == false || !mounted) return false; @@ -250,81 +346,150 @@ class _ShopInBitSettingsViewState extends ConsumerState { return showDialog( context: context, barrierDismissible: true, - builder: (ctx) { + builder: (context) { return StatefulBuilder( builder: (ctx, setDialogState) { final matches = _verifyKeyController.text.trim() == _currentKey; - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Verify your key", style: STextStyles.pageTitleH2(ctx)), - const SizedBox(height: 8), - Text( - "Enter your current customer key to " - "confirm you have saved it.", - style: STextStyles.smallMed14(ctx), - ), - const SizedBox(height: 16), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + + // TODO: this conditional can probably be merged when we have time + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 550, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Verify your key", + style: STextStyles.desktopH3(ctx), + ), + ), + const DesktopDialogCloseButton(), + ], ), - child: TextField( - controller: _verifyKeyController, - focusNode: _verifyKeyFocusNode, - style: STextStyles.field(ctx), - decoration: standardInputDecoration( - "Enter current key", - _verifyKeyFocusNode, - ctx, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your current customer key to " + "confirm you have saved it.", + style: STextStyles.desktopTextExtraExtraSmall(ctx), + ), + const SizedBox(height: 16), + AdaptiveTextField( + labelText: "Enter current key", + controller: _verifyKeyController, + onChangedComprehensive: (_) => + setDialogState(() {}), + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + ctx, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Confirm", + buttonHeight: ButtonHeight.l, + enabled: matches, + onPressed: () => Navigator.of( + ctx, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], ), - onChanged: (_) => setDialogState(() {}), ), - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(ctx) - .extension()! - .getSecondaryEnabledButtonStyle(ctx), - onPressed: () => Navigator.of(ctx).pop(false), - child: Text( - "Cancel", - style: STextStyles.button(ctx).copyWith( - color: Theme.of( - ctx, - ).extension()!.accentColorDark, + ], + ), + ); + } else { + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Verify your key", + style: STextStyles.pageTitleH2(ctx), + ), + const SizedBox(height: 8), + Text( + "Enter your current customer key to " + "confirm you have saved it.", + style: STextStyles.smallMed14(ctx), + ), + const SizedBox(height: 16), + AdaptiveTextField( + labelText: "Enter current key", + controller: _verifyKeyController, + onChangedComprehensive: (_) => setDialogState(() {}), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(ctx) + .extension()! + .getSecondaryEnabledButtonStyle(ctx), + onPressed: () => Navigator.of(ctx).pop(false), + child: Text( + "Cancel", + style: STextStyles.button(ctx).copyWith( + color: Theme.of( + ctx, + ).extension()!.accentColorDark, + ), ), ), ), - ), - const SizedBox(width: 8), - Expanded( - child: TextButton( - style: matches - ? Theme.of(ctx) - .extension()! - .getPrimaryEnabledButtonStyle(ctx) - : Theme.of(ctx) - .extension()! - .getPrimaryDisabledButtonStyle(ctx), - onPressed: matches - ? () => Navigator.of(ctx).pop(true) - : null, - child: Text( - "Confirm", - style: STextStyles.button(ctx), + const SizedBox(width: 8), + Expanded( + child: TextButton( + style: matches + ? Theme.of(ctx) + .extension()! + .getPrimaryEnabledButtonStyle(ctx) + : Theme.of(ctx) + .extension()! + .getPrimaryDisabledButtonStyle(ctx), + onPressed: matches + ? () => Navigator.of(ctx).pop(true) + : null, + child: Text( + "Confirm", + style: STextStyles.button(ctx), + ), ), ), - ), - ], - ), - ], - ), - ); + ], + ), + ], + ), + ); + } }, ); }, @@ -333,213 +498,388 @@ class _ShopInBitSettingsViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.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.only(left: 12, top: 12, right: 12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + // TODO: this conditional can probably be merged when we have time + if (Util.isDesktop) { + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(right: 30), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.key, + width: 48, + height: 48, + ), ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Customer Key", - style: STextStyles.titleBold12(context), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Customer Key", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 16), + Text( + "Your customer key identifies you to ShopinBit. " + "Save it to restore access to your conversations " + "on another device. If you change it, you will " + "lose access to existing conversations.", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 20), + if (_currentKey != null) ...[ + Text( + "Current key", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, ), - const SizedBox(height: 8), - Text( - "Your customer key identifies you " - "to ShopinBit. Save it to restore " - "access to your conversations on " - "another device. If you change it, " - "you will lose access to existing " - "conversations.", - style: STextStyles.itemSubtitle12(context), + ), + const SizedBox(height: 8), + Row( + children: [ + SelectableText( + _currentKey!, + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: _currentKey!), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Key copied to clipboard", + context: context, + ), + ); + } + }, + child: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.textDark3, ), - const SizedBox(height: 16), - if (_currentKey != null) ...[ - RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: Row( - children: [ - Expanded( - child: SelectableText( - _currentKey!, - style: STextStyles.field(context), - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () async { - await Clipboard.setData( - ClipboardData( - text: _currentKey!, + ), + ], + ), + const SizedBox(height: 20), + ] else + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + "No key set", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + ), + PrimaryButton( + width: 210, + buttonHeight: ButtonHeight.m, + enabled: !_loading, + label: _currentKey == null + ? "Generate key" + : "Generate new key", + onPressed: _generate, + ), + const SizedBox(height: 20), + Text( + "Restore key", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 8), + Text( + "Enter a previously saved customer key to " + "restore access to your ShopinBit " + "conversations.", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: 512, + child: AdaptiveTextField( + labelText: "Enter customer key", + controller: _manualKeyController, + onChangedComprehensive: (_) => setState(() {}), + ), + ), + const SizedBox(height: 16), + PrimaryButton( + width: 210, + buttonHeight: ButtonHeight.m, + enabled: + !_loading && + _manualKeyController.text.trim().isNotEmpty, + label: "Set key", + onPressed: _setManualKey, + ), + const SizedBox(height: 20), + Text( + "Display Name", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 8), + Text( + "The name ShopinBit staff will see " + "when communicating with you.", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: 512, + child: AdaptiveTextField( + labelText: "Display name", + controller: _displayNameController, + onChangedComprehensive: (_) => setState(() {}), + ), + ), + const SizedBox(height: 16), + PrimaryButton( + width: 210, + buttonHeight: ButtonHeight.m, + enabled: + !_savingName && + _displayNameController.text.trim().isNotEmpty, + label: "Save", + onPressed: _saveDisplayName, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } else { + return Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.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.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Customer Key", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 8), + Text( + "Your customer key identifies you " + "to ShopinBit. Save it to restore " + "access to your conversations on " + "another device. If you change it, " + "you will lose access to existing " + "conversations.", + style: STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox(height: 16), + if (_currentKey != null) ...[ + RoundedContainer( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Row( + children: [ + Expanded( + child: SelectableText( + _currentKey!, + style: STextStyles.field( + context, ), - ); - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: - "Key copied to clipboard", - context: context, + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () async { + await Clipboard.setData( + ClipboardData( + text: _currentKey!, ), ); - } - }, - child: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .textDark3, + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: + "Key copied to clipboard", + context: context, + ), + ); + } + }, + child: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textDark3, + ), ), - ), - ], + ], + ), ), + ] else + Text( + "No key set", + style: STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox(height: 16), + PrimaryButton( + label: _currentKey == null + ? "Generate key" + : "Generate new key", + enabled: !_loading, + onPressed: _generate, ), - ] else - Text( - "No key set", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox(height: 16), - PrimaryButton( - label: _currentKey == null - ? "Generate key" - : "Generate new key", - enabled: !_loading, - onPressed: _generate, - ), - ], + ], + ), ), - ), - const SizedBox(height: 12), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Restore key", - style: STextStyles.titleBold12(context), - ), - const SizedBox(height: 8), - Text( - "Enter a previously saved customer " - "key to restore access to your " - "ShopinBit conversations.", - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Restore key", + style: STextStyles.titleBold12(context), ), - child: TextField( - controller: _manualKeyController, - focusNode: _manualKeyFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter customer key", - _manualKeyFocusNode, + const SizedBox(height: 8), + Text( + "Enter a previously saved customer " + "key to restore access to your " + "ShopinBit conversations.", + style: STextStyles.itemSubtitle12( context, ), - onChanged: (_) => setState(() {}), ), - ), - const SizedBox(height: 12), - PrimaryButton( - label: "Set key", - enabled: - !_loading && - _manualKeyController.text - .trim() - .isNotEmpty, - onPressed: _setManualKey, - ), - ], + const SizedBox(height: 12), + AdaptiveTextField( + labelText: "Enter customer key", + controller: _manualKeyController, + onChangedComprehensive: (_) => + setState(() {}), + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Set key", + enabled: + !_loading && + _manualKeyController.text + .trim() + .isNotEmpty, + onPressed: _setManualKey, + ), + ], + ), ), - ), - const SizedBox(height: 12), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Display Name", - style: STextStyles.titleBold12(context), - ), - const SizedBox(height: 8), - Text( - "The name ShopinBit staff will see " - "when communicating with you.", - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Display Name", + style: STextStyles.titleBold12(context), ), - child: TextField( - controller: _displayNameController, - focusNode: _displayNameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Display name", - _displayNameFocusNode, + const SizedBox(height: 8), + Text( + "The name ShopinBit staff will see " + "when communicating with you.", + style: STextStyles.itemSubtitle12( context, ), - onChanged: (_) => setState(() {}), ), - ), - const SizedBox(height: 12), - PrimaryButton( - label: "Save", - enabled: - !_savingName && - _displayNameController.text - .trim() - .isNotEmpty, - onPressed: _saveDisplayName, - ), - ], + const SizedBox(height: 12), + AdaptiveTextField( + labelText: "Display name", + controller: _displayNameController, + onChangedComprehensive: (_) => + setState(() {}), + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Save", + enabled: + !_savingName && + _displayNameController.text + .trim() + .isNotEmpty, + onPressed: _saveDisplayName, + ), + ], + ), ), - ), - const SizedBox(height: 12), - ], + const SizedBox(height: 12), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ); + ); + } } } diff --git a/lib/pages/shopinbit/shopinbit_setup_view.dart b/lib/pages/shopinbit/shopinbit_setup_view.dart index 5566d5320..1ce525f25 100644 --- a/lib/pages/shopinbit/shopinbit_setup_view.dart +++ b/lib/pages/shopinbit/shopinbit_setup_view.dart @@ -1,20 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/db/drift_provider.dart'; +import '../../providers/global/shopin_bit_service_provider.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 '../../widgets/textfields/adaptive_text_field.dart'; import 'shopinbit_step_2.dart'; -class ShopInBitSetupView extends StatefulWidget { +class ShopInBitSetupView extends ConsumerStatefulWidget { const ShopInBitSetupView({super.key, required this.model}); static const String routeName = "/shopInBitSetup"; @@ -22,44 +23,49 @@ class ShopInBitSetupView extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitSetupViewState(); + ConsumerState createState() => _ShopInBitSetupViewState(); } -class _ShopInBitSetupViewState extends State { +class _ShopInBitSetupViewState extends ConsumerState { late final Future _keyFuture; - late final TextEditingController _nameController; - late final FocusNode _nameFocusNode; + final TextEditingController _nameController = TextEditingController(); bool get _canContinue => _nameController.text.trim().isNotEmpty; @override void initState() { super.initState(); - _keyFuture = ShopInBitService.instance.ensureCustomerKey(); - final existingName = ShopInBitService.instance.loadDisplayName(); - _nameController = TextEditingController(text: existingName ?? ''); - _nameFocusNode = FocusNode(); + _keyFuture = ref.read(pShopinBitService).ensureCustomerKey(); - _nameFocusNode.addListener(() { - setState(() {}); - }); + // not the greatest solution but its the least invasive with the current + // ui code impl + () async { + final settings = await ref + .read(pSharedDrift) + .shopinBitSettingsDao + .getSettings(); + if (mounted) { + setState(() { + _nameController.text = settings.displayName ?? ""; + }); + } + }(); } @override void dispose() { _nameController.dispose(); - _nameFocusNode.dispose(); super.dispose(); } Future _completeSetup() async { final name = _nameController.text.trim(); widget.model.displayName = name; - await ShopInBitService.instance.setDisplayName(name); - await ShopInBitService.instance.setSetupComplete(true); + await ref.read(pSharedDrift).shopinBitSettingsDao.setDisplayName(name); + await ref.read(pSharedDrift).shopinBitSettingsDao.setSetupComplete(true); if (mounted) { - Navigator.of( + await Navigator.of( context, ).pushReplacementNamed(ShopInBitStep2.routeName, arguments: widget.model); } @@ -158,30 +164,12 @@ class _ShopInBitSetupViewState extends State { 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, - ), - ), - ), + AdaptiveTextField( + labelText: "Display name", + controller: _nameController, + autocorrect: false, + enableSuggestions: false, + onChangedComprehensive: (_) => setState(() {}), ), const Spacer(), PrimaryButton( diff --git a/lib/pages/shopinbit/shopinbit_shipping_view.dart b/lib/pages/shopinbit/shopinbit_shipping_view.dart index 013b276da..03ae92354 100644 --- a/lib/pages/shopinbit/shopinbit_shipping_view.dart +++ b/lib/pages/shopinbit/shopinbit_shipping_view.dart @@ -2,10 +2,11 @@ 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 '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../services/shopinbit/src/models/address.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; @@ -20,7 +21,7 @@ import '../../widgets/desktop/primary_button.dart'; import '../../widgets/stack_text_field.dart'; import 'shopinbit_payment_view.dart'; -class ShopInBitShippingView extends StatefulWidget { +class ShopInBitShippingView extends ConsumerStatefulWidget { const ShopInBitShippingView({super.key, required this.model}); static const String routeName = "/shopInBitShipping"; @@ -28,10 +29,11 @@ class ShopInBitShippingView extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitShippingViewState(); + ConsumerState createState() => + _ShopInBitShippingViewState(); } -class _ShopInBitShippingViewState extends State { +class _ShopInBitShippingViewState extends ConsumerState { late final TextEditingController _nameController; late final TextEditingController _streetController; late final TextEditingController _cityController; @@ -150,7 +152,7 @@ class _ShopInBitShippingViewState extends State { Future _fetchCountries() async { setState(() => _loadingCountries = true); try { - final resp = await ShopInBitService.instance.client.getCountries(); + final resp = await ref.read(pShopinBitService).client.getCountries(); if (resp.hasError || resp.value == null) return; _countries = resp.value!; if (_selectedCountryIso != null && @@ -205,18 +207,21 @@ class _ShopInBitShippingViewState extends State { ); } - final resp = await ShopInBitService.instance.client.submitAddress( - widget.model.apiTicketId, - shipping: Address( - firstName: firstName, - lastName: lastName, - street: street, - zip: postalCode, - city: city, - country: country, - ), - billing: billingAddress, - ); + final resp = await ref + .read(pShopinBitService) + .client + .submitAddress( + widget.model.apiTicketId, + shipping: Address( + firstName: firstName, + lastName: lastName, + street: street, + zip: postalCode, + city: city, + country: country, + ), + billing: billingAddress, + ); if (resp.hasError) { // Sandbox may fail here; continue anyway. diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart index b69cc8797..23403ce60 100644 --- a/lib/pages/shopinbit/shopinbit_step_2.dart +++ b/lib/pages/shopinbit/shopinbit_step_2.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; @@ -18,7 +19,7 @@ import '../exchange_view/sub_widgets/step_row.dart'; import 'shopinbit_step_3.dart'; import 'shopinbit_step_4.dart'; -class ShopInBitStep2 extends StatefulWidget { +class ShopInBitStep2 extends ConsumerStatefulWidget { const ShopInBitStep2({super.key, required this.model}); static const String routeName = "/shopInBitStep2"; @@ -26,23 +27,26 @@ class ShopInBitStep2 extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitStep2State(); + ConsumerState createState() => _ShopInBitStep2State(); } -class _ShopInBitStep2State extends State { +class _ShopInBitStep2State extends ConsumerState { ShopInBitCategory? _selected; - void _continue() { + Future _continue() async { widget.model.category = _selected; - final skipGuidelines = ShopInBitService.instance.loadGuidelinesAccepted(); + final skipGuidelines = + (await ref.read(pSharedDrift).shopinBitSettingsDao.getSettings()) + .guidelinesAccepted; + if (!mounted) return; if (skipGuidelines) { widget.model.guidelinesAccepted = true; - Navigator.of( + await Navigator.of( context, ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); } else { - Navigator.of( + await 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 4f6b799c4..f84d487c2 100644 --- a/lib/pages/shopinbit/shopinbit_step_3.dart +++ b/lib/pages/shopinbit/shopinbit_step_3.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; @@ -14,7 +15,7 @@ import '../../widgets/rounded_white_container.dart'; import '../exchange_view/sub_widgets/step_row.dart'; import 'shopinbit_step_4.dart'; -class ShopInBitStep3 extends StatefulWidget { +class ShopInBitStep3 extends ConsumerStatefulWidget { const ShopInBitStep3({super.key, required this.model}); static const String routeName = "/shopInBitStep3"; @@ -22,10 +23,10 @@ class ShopInBitStep3 extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitStep3State(); + ConsumerState createState() => _ShopInBitStep3State(); } -class _ShopInBitStep3State extends State { +class _ShopInBitStep3State extends ConsumerState { bool _agreed = false; String _guidelinesText() { @@ -76,7 +77,7 @@ class _ShopInBitStep3State extends State { void _continue() { widget.model.guidelinesAccepted = true; // Persist acceptance. - ShopInBitService.instance.setGuidelinesAccepted(true); + ref.read(pSharedDrift).shopinBitSettingsDao.setGuidelinesAccepted(true); Navigator.of( context, diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 671ee89d0..d8c8123d1 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; @@ -19,7 +20,7 @@ import '../../widgets/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_offer_view.dart'; -class ShopInBitTicketDetail extends StatefulWidget { +class ShopInBitTicketDetail extends ConsumerStatefulWidget { const ShopInBitTicketDetail({super.key, required this.model}); static const String routeName = "/shopInBitTicketDetail"; @@ -27,10 +28,11 @@ class ShopInBitTicketDetail extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitTicketDetailState(); + ConsumerState createState() => + _ShopInBitTicketDetailState(); } -class _ShopInBitTicketDetailState extends State { +class _ShopInBitTicketDetailState extends ConsumerState { late final TextEditingController _messageController; bool _sending = false; @@ -65,7 +67,7 @@ class _ShopInBitTicketDetailState extends State { Future _loadFromApi() async { setState(() => _loading = true); try { - final client = ShopInBitService.instance.client; + final client = ref.read(pShopinBitService).client; final id = widget.model.apiTicketId; final messagesResp = await client.getMessages(id); @@ -129,10 +131,10 @@ class _ShopInBitTicketDetailState extends State { try { if (widget.model.apiTicketId != 0) { - await ShopInBitService.instance.client.sendMessage( - widget.model.apiTicketId, - text, - ); + await ref + .read(pShopinBitService) + .client + .sendMessage(widget.model.apiTicketId, text); // Reload messages from API to get accurate state await _loadFromApi(); } @@ -152,18 +154,21 @@ class _ShopInBitTicketDetailState extends State { try { final model = widget.model; - final customerKey = await ShopInBitService.instance.ensureCustomerKey(); + final customerKey = await ref.read(pShopinBitService).ensureCustomerKey(); final comment = "${model.requestDescription}\n\n" "The Client paid the car research fee (#${model.feeTicketNumber})"; - final reqResp = await ShopInBitService.instance.client.createRequest( - customerPseudonym: model.displayName, - externalCustomerKey: customerKey, - serviceType: "car_research", - comment: comment, - deliveryCountry: model.deliveryCountry, - ); + final reqResp = await ref + .read(pShopinBitService) + .client + .createRequest( + customerPseudonym: model.displayName, + externalCustomerKey: customerKey, + serviceType: "car_research", + comment: comment, + deliveryCountry: model.deliveryCountry, + ); if (reqResp.hasError || reqResp.value == null) { if (mounted) { diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index 0ff73b681..a7a14024a 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/shopinbit_ticket.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../services/shopinbit/shopinbit_service.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../services/shopinbit/src/models/car_research.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; @@ -21,16 +22,17 @@ import 'shopinbit_car_fee_view.dart'; import 'shopinbit_car_research_payment_view.dart'; import 'shopinbit_ticket_detail.dart'; -class ShopInBitTicketsView extends StatefulWidget { +class ShopInBitTicketsView extends ConsumerStatefulWidget { const ShopInBitTicketsView({super.key}); static const String routeName = "/shopInBitTickets"; @override - State createState() => _ShopInBitTicketsViewState(); + ConsumerState createState() => + _ShopInBitTicketsViewState(); } -class _ShopInBitTicketsViewState extends State { +class _ShopInBitTicketsViewState extends ConsumerState { List _tickets = []; bool _syncing = false; ShopInBitTicket? _pendingTicket; @@ -112,7 +114,7 @@ class _ShopInBitTicketsViewState extends State { Future _syncFromApi() async { setState(() => _syncing = true); try { - final service = ShopInBitService.instance; + final service = ref.read(pShopinBitService); final customerKey = await service.ensureCustomerKey(); final resp = await service.client.getTicketsByCustomer(customerKey); 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 282d82613..993a773d7 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart @@ -1,7 +1,9 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../utilities/util.dart"; import "shopinbit_country_picker.dart"; import "shopinbit_labeled_checkbox.dart"; @@ -17,16 +19,18 @@ const List _conciergeConditions = ["NEW", "USED"]; const int _minConciergeBudget = 1000; const int _maxConciergeBudget = 100000; -class ShopInBitConciergeForm extends StatefulWidget { +class ShopInBitConciergeForm extends ConsumerStatefulWidget { const ShopInBitConciergeForm({super.key, required this.model}); final ShopInBitOrderModel model; @override - State createState() => _ShopInBitConciergeFormState(); + ConsumerState createState() => + _ShopInBitConciergeFormState(); } -class _ShopInBitConciergeFormState extends State { +class _ShopInBitConciergeFormState + extends ConsumerState { final TextEditingController _whatToPurchaseController = TextEditingController(); final FocusNode _whatToPurchaseFocusNode = FocusNode(); @@ -103,7 +107,11 @@ class _ShopInBitConciergeFormState extends State { ..deliveryCountry = countryIso; try { - await submitShopInBitRequest(context, widget.model); + await submitShopInBitRequest( + context, + widget.model, + ref.read(pShopinBitService), + ); } finally { if (mounted) setState(() => _submitting = false); } 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 f5ae405b2..f0feb9db6 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart @@ -1,15 +1,16 @@ 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 "../../../services/shopinbit/shopinbit_service.dart"; +import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../themes/stack_colors.dart"; import "../../../utilities/assets.dart"; import "../../../utilities/constants.dart"; import "../../../utilities/text_styles.dart"; import "../../../utilities/util.dart"; -class ShopInBitCountryPicker extends StatefulWidget { +class ShopInBitCountryPicker extends ConsumerStatefulWidget { const ShopInBitCountryPicker({ super.key, required this.selectedIso, @@ -22,10 +23,12 @@ class ShopInBitCountryPicker extends StatefulWidget { final String hintText; @override - State createState() => _ShopInBitCountryPickerState(); + ConsumerState createState() => + _ShopInBitCountryPickerState(); } -class _ShopInBitCountryPickerState extends State { +class _ShopInBitCountryPickerState + extends ConsumerState { final TextEditingController _searchController = TextEditingController(); List> _countries = []; bool _loading = false; @@ -45,7 +48,7 @@ class _ShopInBitCountryPickerState extends State { Future _fetchCountries() async { setState(() => _loading = true); try { - final resp = await ShopInBitService.instance.client.getCountries(); + final resp = await ref.read(pShopinBitService).client.getCountries(); if (resp.hasError || resp.value == null) return; _countries = resp.value!; if (widget.selectedIso != null && diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart index f31741f19..015e94d13 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart @@ -1,6 +1,8 @@ import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../utilities/util.dart"; import "shopinbit_country_picker.dart"; import "shopinbit_privacy_checkbox.dart"; @@ -14,16 +16,17 @@ import "shopinbit_step4_text_field.dart"; /// /// Note: the original code used the travel copy for this fallback; that /// behaviour is preserved here. -class ShopInBitGenericForm extends StatefulWidget { +class ShopInBitGenericForm extends ConsumerStatefulWidget { const ShopInBitGenericForm({super.key, required this.model}); final ShopInBitOrderModel model; @override - State createState() => _ShopInBitGenericFormState(); + ConsumerState createState() => + _ShopInBitGenericFormState(); } -class _ShopInBitGenericFormState extends State { +class _ShopInBitGenericFormState extends ConsumerState { late final TextEditingController _descriptionController; final FocusNode _descriptionFocusNode = FocusNode(); @@ -63,7 +66,11 @@ class _ShopInBitGenericFormState extends State { ..requestDescription = _descriptionController.text.trim() ..deliveryCountry = _selectedCountryIso!; try { - await submitShopInBitRequest(context, widget.model); + await submitShopInBitRequest( + context, + widget.model, + ref.read(pShopinBitService), + ); } finally { if (mounted) setState(() => _submitting = false); } diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart index ede142409..1f0e798b9 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart @@ -17,9 +17,9 @@ import "../shopinbit_order_created.dart"; Future submitShopInBitRequest( BuildContext context, ShopInBitOrderModel model, + ShopInBitService service, ) async { try { - final ShopInBitService service = ShopInBitService.instance; final String customerKey = await service.ensureCustomerKey(); assert( 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 bfddd184a..b84885a4e 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart @@ -1,7 +1,9 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../utilities/text_styles.dart"; import "../../../utilities/util.dart"; import "shopinbit_country_picker.dart"; @@ -54,16 +56,17 @@ const int _minArrangementDetailsLength = 10; /// Travel request form. Collects arrangement type, departure / destinations, /// dates (either exact or flexible), travelers and budget, then submits via /// the shared submit helper. -class ShopInBitTravelForm extends StatefulWidget { +class ShopInBitTravelForm extends ConsumerStatefulWidget { const ShopInBitTravelForm({super.key, required this.model}); final ShopInBitOrderModel model; @override - State createState() => _ShopInBitTravelFormState(); + ConsumerState createState() => + _ShopInBitTravelFormState(); } -class _ShopInBitTravelFormState extends State { +class _ShopInBitTravelFormState extends ConsumerState { final TextEditingController _arrangementDetailsController = TextEditingController(); final FocusNode _arrangementDetailsFocusNode = FocusNode(); @@ -271,7 +274,11 @@ class _ShopInBitTravelFormState extends State { // structured comment field. ..deliveryCountry = "DE"; try { - await submitShopInBitRequest(context, widget.model); + await submitShopInBitRequest( + context, + widget.model, + ref.read(pShopinBitService), + ); } finally { if (mounted) setState(() => _submitting = false); } diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index 956a77cd7..b42fb080b 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -11,8 +11,9 @@ import '../../../models/shopinbit/shopinbit_order_model.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../pages/shopinbit/shopinbit_step_1.dart'; import '../../../pages/shopinbit/shopinbit_tickets_view.dart'; +import '../../../providers/db/drift_provider.dart'; import '../../../providers/desktop/current_desktop_menu_item.dart'; -import '../../../services/shopinbit/shopinbit_service.dart'; +import '../../../providers/global/shopin_bit_service_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/text_styles.dart'; @@ -90,12 +91,16 @@ class _DesktopServicesViewState extends ConsumerState { return shouldContinue ?? false; } - Future _showShopDialog(BuildContext context) async { - final service = ShopInBitService.instance; + Future _showShopDialog() async { + final dao = ref.read(pSharedDrift).shopinBitSettingsDao; + final settings = await dao.getSettings(); final model = ShopInBitOrderModel(); bool isFirstRun = false; - if (!service.loadSetupComplete()) { + if (!settings.setupComplete) { + // something went wrong + if (!mounted) return; + // First-time user: show setup. final completed = await showDialog( context: context, @@ -106,13 +111,13 @@ class _DesktopServicesViewState extends ConsumerState { isFirstRun = true; } else { // Returning user: restore display name. - final savedName = service.loadDisplayName(); + final savedName = settings.displayName; if (savedName != null && savedName.isNotEmpty) { model.displayName = savedName; } } - if (!context.mounted) return; + if (!mounted) return; if (isFirstRun) { // First run: show service overview then go directly to Step2 @@ -239,7 +244,7 @@ class _DesktopServicesViewState extends ConsumerState { buttonHeight: ButtonHeight.m, enabled: true, label: "Shop with ShopinBit", - onPressed: () => _showShopDialog(context), + onPressed: _showShopDialog, ), const SizedBox(width: 16), Builder( @@ -296,29 +301,41 @@ class _DesktopServicesViewState extends ConsumerState { } } -class _ShopInBitDesktopSetupDialog extends StatefulWidget { +class _ShopInBitDesktopSetupDialog extends ConsumerStatefulWidget { const _ShopInBitDesktopSetupDialog({required this.model}); final ShopInBitOrderModel model; @override - State<_ShopInBitDesktopSetupDialog> createState() => + ConsumerState<_ShopInBitDesktopSetupDialog> createState() => _ShopInBitDesktopSetupDialogState(); } class _ShopInBitDesktopSetupDialogState - extends State<_ShopInBitDesktopSetupDialog> { + extends ConsumerState<_ShopInBitDesktopSetupDialog> { late final Future _keyFuture; - late final TextEditingController _nameController; + final TextEditingController _nameController = TextEditingController(); bool get _canContinue => _nameController.text.trim().isNotEmpty; @override void initState() { super.initState(); - _keyFuture = ShopInBitService.instance.ensureCustomerKey(); - final existingName = ShopInBitService.instance.loadDisplayName(); - _nameController = TextEditingController(text: existingName ?? ''); + _keyFuture = ref.read(pShopinBitService).ensureCustomerKey(); + + // not the greatest solution but its the least invasive with the current + // ui code impl + () async { + final settings = await ref + .read(pSharedDrift) + .shopinBitSettingsDao + .getSettings(); + if (mounted) { + setState(() { + _nameController.text = settings.displayName ?? ""; + }); + } + }(); } @override @@ -330,8 +347,9 @@ class _ShopInBitDesktopSetupDialogState Future _completeSetup() async { final name = _nameController.text.trim(); widget.model.displayName = name; - await ShopInBitService.instance.setDisplayName(name); - await ShopInBitService.instance.setSetupComplete(true); + final dao = ref.read(pSharedDrift).shopinBitSettingsDao; + await dao.setDisplayName(name); + await dao.setSetupComplete(true); if (mounted) { Navigator.of(context, rootNavigator: true).pop(true); } diff --git a/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart index 69b6dfffb..b693f5d3f 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart @@ -19,45 +19,49 @@ class DesktopShopinBitFirstRun extends StatelessWidget { return SDialog( child: SizedBox( width: 580, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("ShopinBit", style: STextStyles.desktopH2(context)), - const SizedBox(height: 16), - RichText( - text: TextSpan( - style: STextStyles.desktopTextSmall(context), - children: const [ - TextSpan( - text: - "Please note the following before proceeding:" - "\n\n\u2022 Minimum order amount: 1,000 EUR" - "\n\u2022 Service fee: 10% of the order total", + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("ShopinBit", style: STextStyles.desktopH2(context)), + const SizedBox(height: 24), + RichText( + text: TextSpan( + style: STextStyles.desktopTextSmall(context), + children: const [ + TextSpan( + text: + "Please note the following before proceeding:" + "\n\n\u2022 Minimum order amount: 1,000 EUR" + "\n\u2022 Service fee: 10% of the order total", + ), + ], + ), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SecondaryButton( + width: 220, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + PrimaryButton( + width: 220, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () => Navigator.of(context).pushReplacementNamed( + ShopInBitStep1.routeName, + arguments: model, + ), ), ], ), - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SecondaryButton( - width: 220, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: Navigator.of(context).pop, - ), - PrimaryButton( - width: 220, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () => Navigator.of( - context, - ).pushNamed(ShopInBitStep1.routeName, arguments: model), - ), - ], - ), - ], + ], + ), ), ), ); diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart index 424718696..ee2c423b3 100644 --- a/lib/pages_desktop_specific/settings/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/settings/desktop_settings_view.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app_config.dart'; +import '../../pages/shopinbit/shopinbit_settings_view.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; @@ -26,7 +27,6 @@ import 'settings_menu/currency_settings/currency_settings.dart'; import 'settings_menu/language_settings/language_settings.dart'; import 'settings_menu/nodes_settings.dart'; import 'settings_menu/security_settings.dart'; -import 'settings_menu/shopinbit_settings.dart'; import 'settings_menu/syncing_preferences_settings.dart'; import 'settings_menu/tor_settings/tor_settings.dart'; @@ -98,7 +98,7 @@ class _DesktopSettingsViewState extends ConsumerState { const Navigator( key: Key("settingsShopInBitDesktopKey"), onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: ShopInBitDesktopSettings.routeName, + initialRoute: ShopInBitSettingsView.routeName, ), //shopinbit ]; return DesktopScaffold( diff --git a/lib/pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart deleted file mode 100644 index 146243c96..000000000 --- a/lib/pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart +++ /dev/null @@ -1,550 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; - -import '../../../notifications/show_flush_bar.dart'; -import '../../../services/shopinbit/shopinbit_service.dart'; -import '../../../themes/stack_colors.dart'; -import '../../../utilities/assets.dart'; -import '../../../utilities/constants.dart'; -import '../../../utilities/text_styles.dart'; -import '../../../widgets/desktop/desktop_dialog.dart'; -import '../../../widgets/desktop/desktop_dialog_close_button.dart'; -import '../../../widgets/desktop/primary_button.dart'; -import '../../../widgets/desktop/secondary_button.dart'; -import '../../../widgets/rounded_white_container.dart'; -import '../../../widgets/stack_text_field.dart'; - -class ShopInBitDesktopSettings extends ConsumerStatefulWidget { - const ShopInBitDesktopSettings({super.key}); - - static const String routeName = "/settingsMenuShopInBit"; - - @override - ConsumerState createState() => - _ShopInBitDesktopSettingsState(); -} - -class _ShopInBitDesktopSettingsState - extends ConsumerState { - final _manualKeyController = TextEditingController(); - final _manualKeyFocusNode = FocusNode(); - final _verifyKeyController = TextEditingController(); - final _verifyKeyFocusNode = FocusNode(); - late final TextEditingController _displayNameController; - late final FocusNode _displayNameFocusNode; - - String? _currentKey; - bool _loading = false; - bool _savingName = false; - - @override - void initState() { - super.initState(); - _currentKey = ShopInBitService.instance.loadCustomerKey(); - final savedName = ShopInBitService.instance.loadDisplayName(); - _displayNameController = TextEditingController(text: savedName ?? ''); - _displayNameFocusNode = FocusNode(); - } - - @override - void dispose() { - _manualKeyController.dispose(); - _manualKeyFocusNode.dispose(); - _verifyKeyController.dispose(); - _verifyKeyFocusNode.dispose(); - _displayNameController.dispose(); - _displayNameFocusNode.dispose(); - super.dispose(); - } - - Future _saveDisplayName() async { - final name = _displayNameController.text.trim(); - if (name.isEmpty) return; - setState(() => _savingName = true); - try { - await ShopInBitService.instance.setDisplayName(name); - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Display name updated", - context: context, - ), - ); - } - } finally { - if (mounted) setState(() => _savingName = false); - } - } - - Future _generate() async { - if (_currentKey != null) { - final proceed = await _showChangeWarning(); - if (proceed != true) return; - } - - setState(() => _loading = true); - try { - final String key; - if (_currentKey != null) { - final resp = await ShopInBitService.instance.client.generateKey(); - key = resp.valueOrThrow; - await ShopInBitService.instance.setCustomerKey(key); - } else { - key = await ShopInBitService.instance.ensureCustomerKey(); - } - setState(() => _currentKey = key); - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Customer key generated", - context: context, - ), - ); - } - } catch (e) { - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to generate key: $e", - context: context, - ), - ); - } - } finally { - setState(() => _loading = false); - } - } - - Future _setManualKey() async { - final newKey = _manualKeyController.text.trim(); - if (newKey.isEmpty) return; - - if (_currentKey != null) { - final proceed = await _showChangeWarning(); - if (proceed != true) return; - } - - setState(() => _loading = true); - try { - await ShopInBitService.instance.setCustomerKey(newKey); - setState(() { - _currentKey = newKey; - _manualKeyController.clear(); - }); - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Customer key set", - context: context, - ), - ); - } - } catch (e) { - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to set key: $e", - context: context, - ), - ); - } - } finally { - setState(() => _loading = false); - } - } - - Future _showChangeWarning() async { - final result = await showDialog( - context: context, - barrierDismissible: true, - builder: (ctx) => DesktopDialog( - maxWidth: 550, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Save your current key", - style: STextStyles.desktopH3(ctx), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Your current customer key is:", - style: STextStyles.desktopTextExtraExtraSmall(ctx), - ), - const SizedBox(height: 8), - RoundedWhiteContainer( - borderColor: Theme.of( - ctx, - ).extension()!.textSubtitle6, - child: SelectableText( - _currentKey!, - style: STextStyles.desktopTextSmall(ctx), - ), - ), - const SizedBox(height: 16), - Text( - "Changing your key will disconnect you from " - "existing ShopinBit requests. Make sure " - "you have saved your current key before " - "proceeding.", - style: STextStyles.desktopTextExtraExtraSmall(ctx), - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: () => - Navigator.of(ctx, rootNavigator: true).pop(false), - ), - ), - const SizedBox(width: 16), - Expanded( - child: PrimaryButton( - label: "I saved my key", - buttonHeight: ButtonHeight.l, - onPressed: () => - Navigator.of(ctx, rootNavigator: true).pop(null), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - - if (result == false || !mounted) return false; - - return _showVerifyDialog(); - } - - Future _showVerifyDialog() async { - _verifyKeyController.clear(); - return showDialog( - context: context, - barrierDismissible: true, - builder: (ctx) { - return StatefulBuilder( - builder: (ctx, setDialogState) { - final matches = _verifyKeyController.text.trim() == _currentKey; - return DesktopDialog( - maxWidth: 550, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Verify your key", - style: STextStyles.desktopH3(ctx), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter your current customer key to " - "confirm you have saved it.", - style: STextStyles.desktopTextExtraExtraSmall(ctx), - ), - const SizedBox(height: 16), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _verifyKeyController, - focusNode: _verifyKeyFocusNode, - style: STextStyles.field(ctx), - decoration: standardInputDecoration( - "Enter current key", - _verifyKeyFocusNode, - ctx, - ), - onChanged: (_) => setDialogState(() {}), - ), - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - ctx, - rootNavigator: true, - ).pop(false), - ), - ), - const SizedBox(width: 16), - Expanded( - child: PrimaryButton( - label: "Confirm", - buttonHeight: ButtonHeight.l, - enabled: matches, - onPressed: () => Navigator.of( - ctx, - rootNavigator: true, - ).pop(true), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ); - }, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(right: 30), - child: RoundedWhiteContainer( - radiusMultiplier: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - Assets.svg.key, - width: 48, - height: 48, - ), - ), - Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Customer Key", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 16), - Text( - "Your customer key identifies you to ShopinBit. " - "Save it to restore access to your conversations " - "on another device. If you change it, you will " - "lose access to existing conversations.", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox(height: 20), - if (_currentKey != null) ...[ - Text( - "Current key", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - SelectableText( - _currentKey!, - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () async { - await Clipboard.setData( - ClipboardData(text: _currentKey!), - ); - if (mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Key copied to clipboard", - context: context, - ), - ); - } - }, - child: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of( - context, - ).extension()!.textDark3, - ), - ), - ], - ), - const SizedBox(height: 20), - ] else - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - "No key set", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - ), - PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: !_loading, - label: _currentKey == null - ? "Generate key" - : "Generate new key", - onPressed: _generate, - ), - const SizedBox(height: 20), - Text( - "Restore key", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 8), - Text( - "Enter a previously saved customer key to " - "restore access to your ShopinBit " - "conversations.", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox(height: 16), - SizedBox( - width: 512, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _manualKeyController, - focusNode: _manualKeyFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter customer key", - _manualKeyFocusNode, - context, - ), - onChanged: (_) => setState(() {}), - ), - ), - ), - const SizedBox(height: 16), - PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: - !_loading && - _manualKeyController.text.trim().isNotEmpty, - label: "Set key", - onPressed: _setManualKey, - ), - const SizedBox(height: 20), - Text( - "Display Name", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 8), - Text( - "The name ShopinBit staff will see " - "when communicating with you.", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox(height: 16), - SizedBox( - width: 512, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _displayNameController, - focusNode: _displayNameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Display name", - _displayNameFocusNode, - context, - ), - onChanged: (_) => setState(() {}), - ), - ), - ), - const SizedBox(height: 16), - PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: - !_savingName && - _displayNameController.text.trim().isNotEmpty, - label: "Save", - onPressed: _saveDisplayName, - ), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/providers/db/drift_provider.dart b/lib/providers/db/drift_provider.dart index 658dd5bc7..9f6ea4c35 100644 --- a/lib/providers/db/drift_provider.dart +++ b/lib/providers/db/drift_provider.dart @@ -10,8 +10,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../db/drift/database.dart'; +import '../../db/drift/database.dart' show WalletDatabase, Drift; +import '../../db/drift/shared_database.dart' show SharedDrift; final pDrift = Provider.family( (ref, walletId) => Drift.get(walletId), ); + +final pSharedDrift = Provider((_) => SharedDrift.get()); diff --git a/lib/providers/global/shopin_bit_service_provider.dart b/lib/providers/global/shopin_bit_service_provider.dart new file mode 100644 index 000000000..9f9c422e6 --- /dev/null +++ b/lib/providers/global/shopin_bit_service_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../services/shopinbit/shopinbit_service.dart'; +import 'secure_store_provider.dart'; + +final pShopinBitService = Provider( + (ref) => ShopInBitService()..ensureInitialized(ref.read(secureStoreProvider)), +); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d08cdb165..5aa23961d 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -254,7 +254,6 @@ import 'pages_desktop_specific/settings/settings_menu/desktop_support_view.dart' import 'pages_desktop_specific/settings/settings_menu/language_settings/language_settings.dart'; import 'pages_desktop_specific/settings/settings_menu/nodes_settings.dart'; import 'pages_desktop_specific/settings/settings_menu/security_settings.dart'; -import 'pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart'; import 'pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart'; import 'pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart'; import 'pages_desktop_specific/spark_coins/spark_coins_view.dart'; @@ -2737,13 +2736,6 @@ class RouteGenerator { settings: RouteSettings(name: settings.name), ); - case ShopInBitDesktopSettings.routeName: - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const ShopInBitDesktopSettings(), - settings: RouteSettings(name: settings.name), - ); - case DesktopSupportView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/shopinbit/shopinbit_service.dart b/lib/services/shopinbit/shopinbit_service.dart index b0669433a..d8d2cee31 100644 --- a/lib/services/shopinbit/shopinbit_service.dart +++ b/lib/services/shopinbit/shopinbit_service.dart @@ -1,159 +1,63 @@ -import '../../db/hive/db.dart'; import '../../external_api_keys.dart'; +import '../../utilities/flutter_secure_storage_interface.dart'; import '../../utilities/logger.dart'; import 'src/client.dart'; -class ShopInBitService { - static final instance = ShopInBitService._(); - ShopInBitService._(); +const _kShopinBitCustomerKeyKeySecureStore = "shopinBitSecStoreCustomerKeyKey"; - ShopInBitClient? _client; - String? _customerKey; - bool? _guidelinesAccepted; - bool? _setupComplete; - String? _displayName; +class ShopInBitService { + SecureStorageInterface? _secureStorageInterface; - ShopInBitClient get client { - if (_client == null) { - _client = ShopInBitClient( - accessKey: kShopInBitAccessKey, - partnerSecret: kShopInBitPartnerSecret, - sandbox: true, - ); - // Pre-load customer key for ticket detail API calls. - loadCustomerKey(); + SecureStorageInterface get _secure { + if (_secureStorageInterface == null) { + throw Exception("Did you forget to call ShopInBitService.init()?"); } - return _client!; + return _secureStorageInterface!; } - String? get customerKey => _customerKey; + /// If secure storage was already set, this function will do nothing + void ensureInitialized(SecureStorageInterface secureStore) { + _secureStorageInterface ??= secureStore; + } - String? loadCustomerKey() { - if (_customerKey != null) return _customerKey; - _customerKey = - DB.instance.get( - boxName: DB.boxNamePrefs, - key: "shopInBitCustomerKey", - ) - as String?; - if (_customerKey != null) { - client.externalCustomerKey = _customerKey; - } - return _customerKey; + ShopInBitClient? _client; + ShopInBitClient get client { + _client ??= ShopInBitClient( + accessKey: kShopInBitAccessKey, + partnerSecret: kShopInBitPartnerSecret, + sandbox: true, + ); + return _client!; } + Future loadCustomerKey() => + _secure.read(key: _kShopinBitCustomerKeyKeySecureStore); + Future ensureCustomerKey() async { - if (_customerKey != null) return _customerKey!; - _customerKey = - DB.instance.get( - boxName: DB.boxNamePrefs, - key: "shopInBitCustomerKey", - ) - as String?; - if (_customerKey != null) { + final currentKey = await loadCustomerKey(); + + if (currentKey != null) { Logging.instance.t("ShopInBitService: loaded customer key from DB"); - client.externalCustomerKey = _customerKey; - return _customerKey!; + client.externalCustomerKey = currentKey; + return currentKey; } Logging.instance.i("ShopInBitService: generating new customer key"); final resp = await client.generateKey(); - _customerKey = resp.valueOrThrow; - client.externalCustomerKey = _customerKey; - await DB.instance.put( - boxName: DB.boxNamePrefs, - key: "shopInBitCustomerKey", - value: _customerKey, - ); + final customerKey = resp.valueOrThrow; + await setCustomerKey(customerKey); Logging.instance.i("ShopInBitService: customer key stored"); - return _customerKey!; + return customerKey; } Future setCustomerKey(String key) async { - _customerKey = key; + await _secure.write(key: _kShopinBitCustomerKeyKeySecureStore, value: key); client.externalCustomerKey = key; - await DB.instance.put( - boxName: DB.boxNamePrefs, - key: "shopInBitCustomerKey", - value: key, - ); - Logging.instance.i("ShopInBitService: customer key manually set"); + Logging.instance.i("ShopInBitService: customer key stored"); } Future clearCustomerKey() async { - _customerKey = null; client.externalCustomerKey = null; - await DB.instance.put( - boxName: DB.boxNamePrefs, - key: "shopInBitCustomerKey", - value: null, - ); + await _secure.delete(key: _kShopinBitCustomerKeyKeySecureStore); Logging.instance.i("ShopInBitService: customer key cleared"); } - - bool loadGuidelinesAccepted() { - if (_guidelinesAccepted != null) return _guidelinesAccepted!; - _guidelinesAccepted = - DB.instance.get( - boxName: DB.boxNamePrefs, - key: "shopInBitGuidelinesAccepted", - ) - as bool? ?? - false; - return _guidelinesAccepted!; - } - - Future setGuidelinesAccepted(bool accepted) async { - _guidelinesAccepted = accepted; - await DB.instance.put( - 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( - boxName: DB.boxNamePrefs, - key: "shopInBitSetupComplete", - ) - as bool? ?? - false; - return _setupComplete!; - } - - Future setSetupComplete(bool complete) async { - _setupComplete = complete; - await DB.instance.put( - 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( - boxName: DB.boxNamePrefs, - key: "shopInBitDisplayName", - ) - as String?; - return _displayName; - } - - Future setDisplayName(String name) async { - _displayName = name; - await DB.instance.put( - boxName: DB.boxNamePrefs, - key: "shopInBitDisplayName", - value: name, - ); - Logging.instance.i("ShopInBitService: display name set"); - } } diff --git a/test/services/paynym/paynym_is_api_test.mocks.dart b/test/services/paynym/paynym_is_api_test.mocks.dart index e3d6837fa..c62d8cc0c 100644 --- a/test/services/paynym/paynym_is_api_test.mocks.dart +++ b/test/services/paynym/paynym_is_api_test.mocks.dart @@ -96,4 +96,57 @@ class MockHTTP extends _i1.Mock implements _i2.HTTP { ), ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch({ + required Uri? url, + Map? headers, + Object? body, + required ({_i4.InternetAddress host, int port})? proxyInfo, + }) => + (super.noSuchMethod( + Invocation.method(#patch, [], { + #url: url, + #headers: headers, + #body: body, + #proxyInfo: proxyInfo, + }), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#patch, [], { + #url: url, + #headers: headers, + #body: body, + #proxyInfo: proxyInfo, + }), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete({ + required Uri? url, + Map? headers, + required ({_i4.InternetAddress host, int port})? proxyInfo, + }) => + (super.noSuchMethod( + Invocation.method(#delete, [], { + #url: url, + #headers: headers, + #proxyInfo: proxyInfo, + }), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#delete, [], { + #url: url, + #headers: headers, + #proxyInfo: proxyInfo, + }), + ), + ), + ) + as _i3.Future<_i2.Response>); } From 8ed3d3390013108d9f34e42fb3a5eb9d4a2dff2c Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 17:14:42 -0600 Subject: [PATCH 14/27] chore: add some toString()s --- lib/services/shopinbit/src/models/ticket.dart | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/services/shopinbit/src/models/ticket.dart b/lib/services/shopinbit/src/models/ticket.dart index 2f8e91d06..06093ff21 100644 --- a/lib/services/shopinbit/src/models/ticket.dart +++ b/lib/services/shopinbit/src/models/ticket.dart @@ -33,6 +33,16 @@ class TicketRef { factory TicketRef.fromJson(Map json) { return TicketRef(id: _toInt(json['id']), number: json['number'].toString()); } + + Map toMap() { + return { + "id": id, + "number": number, + }; + } + + @override + String toString() => toMap().toString(); } class TicketStatus { @@ -64,6 +74,20 @@ class TicketStatus { trackingLink: json['tracking_link'] as String?, ); } + + Map toMap() { + return { + "ticket_id": ticketId, + "state": state.toString(), + "updated_at": updatedAt.toIso8601String(), + "last_agent_message_at": lastAgentMessageAt?.toIso8601String(), + "payment_invoice_status": paymentInvoiceStatus, + "tracking_link": trackingLink, + }; + } + + @override + String toString() => toMap().toString(); } class TicketFull { @@ -102,6 +126,23 @@ class TicketFull { vatRate: _toInt(json['vat_rate']), ); } + + Map toMap() { + return { + "id": id, + "number": number, + "product_name": productName, + "customer_price": customerPrice, + "partner_price": partnerPrice, + "partner_commission": partnerCommission, + "net_purchase_price": netPurchasePrice, + "net_shipping_costs": netShippingCosts, + "vat_rate": vatRate, + }; + } + + @override + String toString() => toMap().toString(); } int _toInt(dynamic value) { From 1c6ffa9af90985ac3ba02d6d604e7383174ebb16 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 17:50:06 -0600 Subject: [PATCH 15/27] fix(ui): clean up flow logic and state issues --- .../shopinbit/shopinbit_settings_view.dart | 296 ++++++++---------- 1 file changed, 136 insertions(+), 160 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_settings_view.dart b/lib/pages/shopinbit/shopinbit_settings_view.dart index 50b62043d..adcbdb2db 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -13,11 +13,13 @@ 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/desktop/secondary_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; @@ -35,7 +37,6 @@ class ShopInBitSettingsView extends ConsumerStatefulWidget { class _ShopInBitSettingsViewState extends ConsumerState { final _manualKeyController = TextEditingController(); - final _verifyKeyController = TextEditingController(); final _displayNameController = TextEditingController(); String? _currentKey; @@ -66,7 +67,6 @@ class _ShopInBitSettingsViewState extends ConsumerState { @override void dispose() { _manualKeyController.dispose(); - _verifyKeyController.dispose(); _displayNameController.dispose(); super.dispose(); } @@ -173,7 +173,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { } Future _showChangeWarning() async { - final result = await showDialog( + final confirmSaved = await showDialog( context: context, builder: (context) { // TODO: this conditional can probably be merged when we have time @@ -237,7 +237,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { onPressed: () => Navigator.of( context, rootNavigator: true, - ).pop(false), + ).pop(), ), ), const SizedBox(width: 16), @@ -248,7 +248,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { onPressed: () => Navigator.of( context, rootNavigator: true, - ).pop(null), + ).pop(true), ), ), ], @@ -303,7 +303,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle(context), - onPressed: () => Navigator.of(context).pop(false), + onPressed: () => Navigator.of(context).pop(), child: Text( "Cancel", style: STextStyles.button(context).copyWith( @@ -320,7 +320,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - onPressed: () => Navigator.of(context).pop(null), + onPressed: () => Navigator.of(context).pop(true), child: Text( "I saved my key", style: STextStyles.button(context), @@ -336,163 +336,12 @@ class _ShopInBitSettingsViewState extends ConsumerState { }, ); - if (result == false || !mounted) return false; - - return _showVerifyDialog(); - } + if (confirmSaved != true || !mounted) return false; - Future _showVerifyDialog() async { - _verifyKeyController.clear(); return showDialog( context: context, barrierDismissible: true, - builder: (context) { - return StatefulBuilder( - builder: (ctx, setDialogState) { - final matches = _verifyKeyController.text.trim() == _currentKey; - - // TODO: this conditional can probably be merged when we have time - if (Util.isDesktop) { - return DesktopDialog( - maxWidth: 550, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Verify your key", - style: STextStyles.desktopH3(ctx), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter your current customer key to " - "confirm you have saved it.", - style: STextStyles.desktopTextExtraExtraSmall(ctx), - ), - const SizedBox(height: 16), - AdaptiveTextField( - labelText: "Enter current key", - controller: _verifyKeyController, - onChangedComprehensive: (_) => - setDialogState(() {}), - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - ctx, - rootNavigator: true, - ).pop(false), - ), - ), - const SizedBox(width: 16), - Expanded( - child: PrimaryButton( - label: "Confirm", - buttonHeight: ButtonHeight.l, - enabled: matches, - onPressed: () => Navigator.of( - ctx, - rootNavigator: true, - ).pop(true), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ); - } else { - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Verify your key", - style: STextStyles.pageTitleH2(ctx), - ), - const SizedBox(height: 8), - Text( - "Enter your current customer key to " - "confirm you have saved it.", - style: STextStyles.smallMed14(ctx), - ), - const SizedBox(height: 16), - AdaptiveTextField( - labelText: "Enter current key", - controller: _verifyKeyController, - onChangedComprehensive: (_) => setDialogState(() {}), - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(ctx) - .extension()! - .getSecondaryEnabledButtonStyle(ctx), - onPressed: () => Navigator.of(ctx).pop(false), - child: Text( - "Cancel", - style: STextStyles.button(ctx).copyWith( - color: Theme.of( - ctx, - ).extension()!.accentColorDark, - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: TextButton( - style: matches - ? Theme.of(ctx) - .extension()! - .getPrimaryEnabledButtonStyle(ctx) - : Theme.of(ctx) - .extension()! - .getPrimaryDisabledButtonStyle(ctx), - onPressed: matches - ? () => Navigator.of(ctx).pop(true) - : null, - child: Text( - "Confirm", - style: STextStyles.button(ctx), - ), - ), - ), - ], - ), - ], - ), - ); - } - }, - ); - }, + builder: (_) => _VerifyKeyDialog(currentKey: _currentKey!), ); } @@ -883,3 +732,130 @@ class _ShopInBitSettingsViewState extends ConsumerState { } } } + +class _VerifyKeyDialog extends StatefulWidget { + const _VerifyKeyDialog({super.key, required this.currentKey}); + + final String currentKey; + + @override + State<_VerifyKeyDialog> createState() => _VerifyKeyDialogState(); +} + +class _VerifyKeyDialogState extends State<_VerifyKeyDialog> { + final _verifyKeyController = TextEditingController(); + + bool _confirmEnabled = false; + + @override + void dispose() { + _verifyKeyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SDialog( + child: SizedBox( + width: 580, + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Verify your key", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), + child: child, + ), + ], + ), + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => StackDialogBase( + child: Column( + mainAxisSize: .min, + children: [ + Text("Verify your key", style: STextStyles.pageTitleH2(context)), + const SizedBox(height: 24), + child, + ], + ), + ), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Enter your current customer key to " + "confirm you have saved it.", + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed14(context), + ), + Util.isDesktop + ? const SizedBox(height: 32) + : const SizedBox(height: 16), + AdaptiveTextField( + labelText: "Enter current key", + controller: _verifyKeyController, + onChangedComprehensive: (_) { + if (_verifyKeyController.text == widget.currentKey) { + if (!_confirmEnabled) setState(() => _confirmEnabled = true); + } else { + if (_confirmEnabled) setState(() => _confirmEnabled = false); + } + }, + ), + Util.isDesktop + ? const SizedBox(height: 32) + : const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Confirm", + buttonHeight: ButtonHeight.l, + enabled: _confirmEnabled, + onPressed: _confirmEnabled + ? () => Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(true) + : null, + ), + ), + ], + ), + ], + ), + ), + ); + } +} From 9dad75d7535260d7e7a4823c04e31d1c72c41cb5 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 17:55:54 -0600 Subject: [PATCH 16/27] fix(ui): button spacing --- lib/pages/shopinbit/shopinbit_settings_view.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_settings_view.dart b/lib/pages/shopinbit/shopinbit_settings_view.dart index adcbdb2db..f6446b635 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -240,7 +240,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { ).pop(), ), ), - const SizedBox(width: 16), + const SizedBox(width: 24), Expanded( child: PrimaryButton( label: "I saved my key", @@ -314,7 +314,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { ), ), ), - const SizedBox(width: 8), + const SizedBox(width: 16), Expanded( child: TextButton( style: Theme.of(context) @@ -837,7 +837,9 @@ class _VerifyKeyDialogState extends State<_VerifyKeyDialog> { ).pop(false), ), ), - const SizedBox(width: 16), + Util.isDesktop + ? const SizedBox(width: 24) + : const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Confirm", From fd53f9fe045226b3e728cfed4772a5ab171cb924 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 17:56:38 -0600 Subject: [PATCH 17/27] fix(ui): check correct context --- lib/pages/shopinbit/shopinbit_settings_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/shopinbit/shopinbit_settings_view.dart b/lib/pages/shopinbit/shopinbit_settings_view.dart index f6446b635..51fb674d5 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -600,7 +600,7 @@ class _ShopInBitSettingsViewState extends ConsumerState { text: _currentKey!, ), ); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, From f357b4a7536ee7c1a26a3ad8d598f22ba0305289 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 May 2026 20:47:08 -0600 Subject: [PATCH 18/27] refactor(db): use drift/sqlite instead of isar --- lib/db/drift/shared_database.g.dart | 845 --- .../{ => shared_db}/shared_database.dart | 85 +- lib/db/drift/shared_db/shared_database.g.dart | 2854 ++++++++++ .../shared_db/tables/cakepay_orders.dart | 8 + .../shared_db/tables/shopin_bit_settings.dart | 15 + .../shared_db/tables/shopin_bit_tickets.dart | 112 + lib/db/isar/main_db.dart | 38 - lib/models/isar/models/isar_models.dart | 1 - lib/models/isar/models/shopinbit_ticket.dart | 51 - .../isar/models/shopinbit_ticket.g.dart | 4651 ----------------- .../shopinbit/shopinbit_order_model.dart | 98 +- lib/pages/more_view/services_view.dart | 15 +- .../global_settings_view/hidden_settings.dart | 33 - .../shopinbit/shopinbit_car_fee_view.dart | 7 +- .../shopinbit_car_research_payment_view.dart | 39 +- .../shopinbit_confirm_send_view.dart | 6 +- .../shopinbit/shopinbit_ticket_detail.dart | 21 +- .../shopinbit/shopinbit_tickets_view.dart | 113 +- .../shopinbit_car_research_form.dart | 18 +- .../shopinbit_concierge_form.dart | 2 + .../shopinbit_generic_form.dart | 2 + .../shopinbit_step4_submit.dart | 7 +- .../shopinbit_travel_form.dart | 2 + .../shopin_bit/desktop_shopinbit_view.dart | 16 +- lib/providers/db/drift_provider.dart | 2 +- lib/services/cakepay/cakepay_service.dart | 2 +- .../car_research_persistence_test.dart | 17 - .../transaction_card_test.mocks.dart | 24 - 28 files changed, 3225 insertions(+), 5859 deletions(-) delete mode 100644 lib/db/drift/shared_database.g.dart rename lib/db/drift/{ => shared_db}/shared_database.dart (78%) create mode 100644 lib/db/drift/shared_db/shared_database.g.dart create mode 100644 lib/db/drift/shared_db/tables/cakepay_orders.dart create mode 100644 lib/db/drift/shared_db/tables/shopin_bit_settings.dart create mode 100644 lib/db/drift/shared_db/tables/shopin_bit_tickets.dart delete mode 100644 lib/models/isar/models/shopinbit_ticket.dart delete mode 100644 lib/models/isar/models/shopinbit_ticket.g.dart diff --git a/lib/db/drift/shared_database.g.dart b/lib/db/drift/shared_database.g.dart deleted file mode 100644 index 9b7d8d7a3..000000000 --- a/lib/db/drift/shared_database.g.dart +++ /dev/null @@ -1,845 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'shared_database.dart'; - -// ignore_for_file: type=lint -mixin _$ShopinBitSettingsDaoMixin on DatabaseAccessor { - $ShopinBitSettingsTable get shopinBitSettings => - attachedDatabase.shopinBitSettings; - ShopinBitSettingsDaoManager get managers => ShopinBitSettingsDaoManager(this); -} - -class ShopinBitSettingsDaoManager { - final _$ShopinBitSettingsDaoMixin _db; - ShopinBitSettingsDaoManager(this._db); - $$ShopinBitSettingsTableTableManager get shopinBitSettings => - $$ShopinBitSettingsTableTableManager( - _db.attachedDatabase, - _db.shopinBitSettings, - ); -} - -class $CakepayOrdersTable extends CakepayOrders - with TableInfo<$CakepayOrdersTable, CakepayOrder> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $CakepayOrdersTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _orderIdMeta = const VerificationMeta( - 'orderId', - ); - @override - late final GeneratedColumn orderId = GeneratedColumn( - 'order_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - @override - List get $columns => [orderId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'cakepay_orders'; - @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('order_id')) { - context.handle( - _orderIdMeta, - orderId.isAcceptableOrUnknown(data['order_id']!, _orderIdMeta), - ); - } else if (isInserting) { - context.missing(_orderIdMeta); - } - return context; - } - - @override - Set get $primaryKey => {orderId}; - @override - CakepayOrder map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CakepayOrder( - orderId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}order_id'], - )!, - ); - } - - @override - $CakepayOrdersTable createAlias(String alias) { - return $CakepayOrdersTable(attachedDatabase, alias); - } -} - -class CakepayOrder extends DataClass implements Insertable { - final String orderId; - const CakepayOrder({required this.orderId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['order_id'] = Variable(orderId); - return map; - } - - CakepayOrdersCompanion toCompanion(bool nullToAbsent) { - return CakepayOrdersCompanion(orderId: Value(orderId)); - } - - factory CakepayOrder.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CakepayOrder(orderId: serializer.fromJson(json['orderId'])); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return {'orderId': serializer.toJson(orderId)}; - } - - CakepayOrder copyWith({String? orderId}) => - CakepayOrder(orderId: orderId ?? this.orderId); - CakepayOrder copyWithCompanion(CakepayOrdersCompanion data) { - return CakepayOrder( - orderId: data.orderId.present ? data.orderId.value : this.orderId, - ); - } - - @override - String toString() { - return (StringBuffer('CakepayOrder(') - ..write('orderId: $orderId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => orderId.hashCode; - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CakepayOrder && other.orderId == this.orderId); -} - -class CakepayOrdersCompanion extends UpdateCompanion { - final Value orderId; - final Value rowid; - const CakepayOrdersCompanion({ - this.orderId = const Value.absent(), - this.rowid = const Value.absent(), - }); - CakepayOrdersCompanion.insert({ - required String orderId, - this.rowid = const Value.absent(), - }) : orderId = Value(orderId); - static Insertable custom({ - Expression? orderId, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (orderId != null) 'order_id': orderId, - if (rowid != null) 'rowid': rowid, - }); - } - - CakepayOrdersCompanion copyWith({Value? orderId, Value? rowid}) { - return CakepayOrdersCompanion( - orderId: orderId ?? this.orderId, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (orderId.present) { - map['order_id'] = Variable(orderId.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CakepayOrdersCompanion(') - ..write('orderId: $orderId, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $ShopinBitSettingsTable extends ShopinBitSettings - with TableInfo<$ShopinBitSettingsTable, ShopinBitSetting> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ShopinBitSettingsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0), - ); - static const VerificationMeta _guidelinesAcceptedMeta = - const VerificationMeta('guidelinesAccepted'); - @override - late final GeneratedColumn guidelinesAccepted = GeneratedColumn( - 'guidelines_accepted', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("guidelines_accepted" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _setupCompleteMeta = const VerificationMeta( - 'setupComplete', - ); - @override - late final GeneratedColumn setupComplete = GeneratedColumn( - 'setup_complete', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("setup_complete" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _displayNameMeta = const VerificationMeta( - 'displayName', - ); - @override - late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - @override - List get $columns => [ - id, - guidelinesAccepted, - setupComplete, - displayName, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'shopin_bit_settings'; - @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('guidelines_accepted')) { - context.handle( - _guidelinesAcceptedMeta, - guidelinesAccepted.isAcceptableOrUnknown( - data['guidelines_accepted']!, - _guidelinesAcceptedMeta, - ), - ); - } - if (data.containsKey('setup_complete')) { - context.handle( - _setupCompleteMeta, - setupComplete.isAcceptableOrUnknown( - data['setup_complete']!, - _setupCompleteMeta, - ), - ); - } - if (data.containsKey('display_name')) { - context.handle( - _displayNameMeta, - displayName.isAcceptableOrUnknown( - data['display_name']!, - _displayNameMeta, - ), - ); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - ShopinBitSetting map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ShopinBitSetting( - id: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}id'], - )!, - guidelinesAccepted: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}guidelines_accepted'], - )!, - setupComplete: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}setup_complete'], - )!, - displayName: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}display_name'], - ), - ); - } - - @override - $ShopinBitSettingsTable createAlias(String alias) { - return $ShopinBitSettingsTable(attachedDatabase, alias); - } -} - -class ShopinBitSetting extends DataClass - implements Insertable { - final int id; - final bool guidelinesAccepted; - final bool setupComplete; - final String? displayName; - const ShopinBitSetting({ - required this.id, - required this.guidelinesAccepted, - required this.setupComplete, - this.displayName, - }); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['guidelines_accepted'] = Variable(guidelinesAccepted); - map['setup_complete'] = Variable(setupComplete); - if (!nullToAbsent || displayName != null) { - map['display_name'] = Variable(displayName); - } - return map; - } - - ShopinBitSettingsCompanion toCompanion(bool nullToAbsent) { - return ShopinBitSettingsCompanion( - id: Value(id), - guidelinesAccepted: Value(guidelinesAccepted), - setupComplete: Value(setupComplete), - displayName: displayName == null && nullToAbsent - ? const Value.absent() - : Value(displayName), - ); - } - - factory ShopinBitSetting.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return ShopinBitSetting( - id: serializer.fromJson(json['id']), - guidelinesAccepted: serializer.fromJson(json['guidelinesAccepted']), - setupComplete: serializer.fromJson(json['setupComplete']), - displayName: serializer.fromJson(json['displayName']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'guidelinesAccepted': serializer.toJson(guidelinesAccepted), - 'setupComplete': serializer.toJson(setupComplete), - 'displayName': serializer.toJson(displayName), - }; - } - - ShopinBitSetting copyWith({ - int? id, - bool? guidelinesAccepted, - bool? setupComplete, - Value displayName = const Value.absent(), - }) => ShopinBitSetting( - id: id ?? this.id, - guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, - setupComplete: setupComplete ?? this.setupComplete, - displayName: displayName.present ? displayName.value : this.displayName, - ); - ShopinBitSetting copyWithCompanion(ShopinBitSettingsCompanion data) { - return ShopinBitSetting( - id: data.id.present ? data.id.value : this.id, - guidelinesAccepted: data.guidelinesAccepted.present - ? data.guidelinesAccepted.value - : this.guidelinesAccepted, - setupComplete: data.setupComplete.present - ? data.setupComplete.value - : this.setupComplete, - displayName: data.displayName.present - ? data.displayName.value - : this.displayName, - ); - } - - @override - String toString() { - return (StringBuffer('ShopinBitSetting(') - ..write('id: $id, ') - ..write('guidelinesAccepted: $guidelinesAccepted, ') - ..write('setupComplete: $setupComplete, ') - ..write('displayName: $displayName') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, guidelinesAccepted, setupComplete, displayName); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ShopinBitSetting && - other.id == this.id && - other.guidelinesAccepted == this.guidelinesAccepted && - other.setupComplete == this.setupComplete && - other.displayName == this.displayName); -} - -class ShopinBitSettingsCompanion extends UpdateCompanion { - final Value id; - final Value guidelinesAccepted; - final Value setupComplete; - final Value displayName; - const ShopinBitSettingsCompanion({ - this.id = const Value.absent(), - this.guidelinesAccepted = const Value.absent(), - this.setupComplete = const Value.absent(), - this.displayName = const Value.absent(), - }); - ShopinBitSettingsCompanion.insert({ - this.id = const Value.absent(), - this.guidelinesAccepted = const Value.absent(), - this.setupComplete = const Value.absent(), - this.displayName = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? guidelinesAccepted, - Expression? setupComplete, - Expression? displayName, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (guidelinesAccepted != null) 'guidelines_accepted': guidelinesAccepted, - if (setupComplete != null) 'setup_complete': setupComplete, - if (displayName != null) 'display_name': displayName, - }); - } - - ShopinBitSettingsCompanion copyWith({ - Value? id, - Value? guidelinesAccepted, - Value? setupComplete, - Value? displayName, - }) { - return ShopinBitSettingsCompanion( - id: id ?? this.id, - guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, - setupComplete: setupComplete ?? this.setupComplete, - displayName: displayName ?? this.displayName, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (guidelinesAccepted.present) { - map['guidelines_accepted'] = Variable(guidelinesAccepted.value); - } - if (setupComplete.present) { - map['setup_complete'] = Variable(setupComplete.value); - } - if (displayName.present) { - map['display_name'] = Variable(displayName.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('ShopinBitSettingsCompanion(') - ..write('id: $id, ') - ..write('guidelinesAccepted: $guidelinesAccepted, ') - ..write('setupComplete: $setupComplete, ') - ..write('displayName: $displayName') - ..write(')')) - .toString(); - } -} - -abstract class _$SharedDatabase extends GeneratedDatabase { - _$SharedDatabase(QueryExecutor e) : super(e); - $SharedDatabaseManager get managers => $SharedDatabaseManager(this); - late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this); - late final $ShopinBitSettingsTable shopinBitSettings = - $ShopinBitSettingsTable(this); - late final ShopinBitSettingsDao shopinBitSettingsDao = ShopinBitSettingsDao( - this as SharedDatabase, - ); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [ - cakepayOrders, - shopinBitSettings, - ]; -} - -typedef $$CakepayOrdersTableCreateCompanionBuilder = - CakepayOrdersCompanion Function({ - required String orderId, - Value rowid, - }); -typedef $$CakepayOrdersTableUpdateCompanionBuilder = - CakepayOrdersCompanion Function({Value orderId, Value rowid}); - -class $$CakepayOrdersTableFilterComposer - extends Composer<_$SharedDatabase, $CakepayOrdersTable> { - $$CakepayOrdersTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get orderId => $composableBuilder( - column: $table.orderId, - builder: (column) => ColumnFilters(column), - ); -} - -class $$CakepayOrdersTableOrderingComposer - extends Composer<_$SharedDatabase, $CakepayOrdersTable> { - $$CakepayOrdersTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get orderId => $composableBuilder( - column: $table.orderId, - builder: (column) => ColumnOrderings(column), - ); -} - -class $$CakepayOrdersTableAnnotationComposer - extends Composer<_$SharedDatabase, $CakepayOrdersTable> { - $$CakepayOrdersTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get orderId => - $composableBuilder(column: $table.orderId, builder: (column) => column); -} - -class $$CakepayOrdersTableTableManager - extends - RootTableManager< - _$SharedDatabase, - $CakepayOrdersTable, - CakepayOrder, - $$CakepayOrdersTableFilterComposer, - $$CakepayOrdersTableOrderingComposer, - $$CakepayOrdersTableAnnotationComposer, - $$CakepayOrdersTableCreateCompanionBuilder, - $$CakepayOrdersTableUpdateCompanionBuilder, - ( - CakepayOrder, - BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, - ), - CakepayOrder, - PrefetchHooks Function() - > { - $$CakepayOrdersTableTableManager( - _$SharedDatabase db, - $CakepayOrdersTable table, - ) : super( - TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $$CakepayOrdersTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$CakepayOrdersTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$CakepayOrdersTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: - ({ - Value orderId = const Value.absent(), - Value rowid = const Value.absent(), - }) => CakepayOrdersCompanion(orderId: orderId, rowid: rowid), - createCompanionCallback: - ({ - required String orderId, - Value rowid = const Value.absent(), - }) => - CakepayOrdersCompanion.insert(orderId: orderId, rowid: rowid), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), - prefetchHooksCallback: null, - ), - ); -} - -typedef $$CakepayOrdersTableProcessedTableManager = - ProcessedTableManager< - _$SharedDatabase, - $CakepayOrdersTable, - CakepayOrder, - $$CakepayOrdersTableFilterComposer, - $$CakepayOrdersTableOrderingComposer, - $$CakepayOrdersTableAnnotationComposer, - $$CakepayOrdersTableCreateCompanionBuilder, - $$CakepayOrdersTableUpdateCompanionBuilder, - ( - CakepayOrder, - BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, - ), - CakepayOrder, - PrefetchHooks Function() - >; -typedef $$ShopinBitSettingsTableCreateCompanionBuilder = - ShopinBitSettingsCompanion Function({ - Value id, - Value guidelinesAccepted, - Value setupComplete, - Value displayName, - }); -typedef $$ShopinBitSettingsTableUpdateCompanionBuilder = - ShopinBitSettingsCompanion Function({ - Value id, - Value guidelinesAccepted, - Value setupComplete, - Value displayName, - }); - -class $$ShopinBitSettingsTableFilterComposer - extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { - $$ShopinBitSettingsTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get guidelinesAccepted => $composableBuilder( - column: $table.guidelinesAccepted, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get setupComplete => $composableBuilder( - column: $table.setupComplete, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => ColumnFilters(column), - ); -} - -class $$ShopinBitSettingsTableOrderingComposer - extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { - $$ShopinBitSettingsTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get guidelinesAccepted => $composableBuilder( - column: $table.guidelinesAccepted, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get setupComplete => $composableBuilder( - column: $table.setupComplete, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => ColumnOrderings(column), - ); -} - -class $$ShopinBitSettingsTableAnnotationComposer - extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { - $$ShopinBitSettingsTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get guidelinesAccepted => $composableBuilder( - column: $table.guidelinesAccepted, - builder: (column) => column, - ); - - GeneratedColumn get setupComplete => $composableBuilder( - column: $table.setupComplete, - builder: (column) => column, - ); - - GeneratedColumn get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => column, - ); -} - -class $$ShopinBitSettingsTableTableManager - extends - RootTableManager< - _$SharedDatabase, - $ShopinBitSettingsTable, - ShopinBitSetting, - $$ShopinBitSettingsTableFilterComposer, - $$ShopinBitSettingsTableOrderingComposer, - $$ShopinBitSettingsTableAnnotationComposer, - $$ShopinBitSettingsTableCreateCompanionBuilder, - $$ShopinBitSettingsTableUpdateCompanionBuilder, - ( - ShopinBitSetting, - BaseReferences< - _$SharedDatabase, - $ShopinBitSettingsTable, - ShopinBitSetting - >, - ), - ShopinBitSetting, - PrefetchHooks Function() - > { - $$ShopinBitSettingsTableTableManager( - _$SharedDatabase db, - $ShopinBitSettingsTable table, - ) : super( - TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $$ShopinBitSettingsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ShopinBitSettingsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ShopinBitSettingsTableAnnotationComposer( - $db: db, - $table: table, - ), - updateCompanionCallback: - ({ - Value id = const Value.absent(), - Value guidelinesAccepted = const Value.absent(), - Value setupComplete = const Value.absent(), - Value displayName = const Value.absent(), - }) => ShopinBitSettingsCompanion( - id: id, - guidelinesAccepted: guidelinesAccepted, - setupComplete: setupComplete, - displayName: displayName, - ), - createCompanionCallback: - ({ - Value id = const Value.absent(), - Value guidelinesAccepted = const Value.absent(), - Value setupComplete = const Value.absent(), - Value displayName = const Value.absent(), - }) => ShopinBitSettingsCompanion.insert( - id: id, - guidelinesAccepted: guidelinesAccepted, - setupComplete: setupComplete, - displayName: displayName, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), - prefetchHooksCallback: null, - ), - ); -} - -typedef $$ShopinBitSettingsTableProcessedTableManager = - ProcessedTableManager< - _$SharedDatabase, - $ShopinBitSettingsTable, - ShopinBitSetting, - $$ShopinBitSettingsTableFilterComposer, - $$ShopinBitSettingsTableOrderingComposer, - $$ShopinBitSettingsTableAnnotationComposer, - $$ShopinBitSettingsTableCreateCompanionBuilder, - $$ShopinBitSettingsTableUpdateCompanionBuilder, - ( - ShopinBitSetting, - BaseReferences< - _$SharedDatabase, - $ShopinBitSettingsTable, - ShopinBitSetting - >, - ), - ShopinBitSetting, - PrefetchHooks Function() - >; - -class $SharedDatabaseManager { - final _$SharedDatabase _db; - $SharedDatabaseManager(this._db); - $$CakepayOrdersTableTableManager get cakepayOrders => - $$CakepayOrdersTableTableManager(_db, _db.cakepayOrders); - $$ShopinBitSettingsTableTableManager get shopinBitSettings => - $$ShopinBitSettingsTableTableManager(_db, _db.shopinBitSettings); -} diff --git a/lib/db/drift/shared_database.dart b/lib/db/drift/shared_db/shared_database.dart similarity index 78% rename from lib/db/drift/shared_database.dart rename to lib/db/drift/shared_db/shared_database.dart index e83028d42..fa6f94e53 100644 --- a/lib/db/drift/shared_database.dart +++ b/lib/db/drift/shared_db/shared_database.dart @@ -2,7 +2,12 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:path/path.dart' as path; -import '../../utilities/stack_file_system.dart'; +import '../../../models/shopinbit/shopinbit_order_model.dart' + show ShopInBitCategory, ShopInBitOrderStatus; +import '../../../utilities/stack_file_system.dart'; +import 'tables/cakepay_orders.dart'; +import 'tables/shopin_bit_settings.dart'; +import 'tables/shopin_bit_tickets.dart'; part 'shared_database.g.dart'; @@ -21,25 +26,39 @@ abstract final class SharedDrift { } } -class CakepayOrders extends Table { - TextColumn get orderId => text()(); +@DriftDatabase( + tables: [CakepayOrders, ShopinBitSettings, ShopInBitTickets], + daos: [ShopinBitSettingsDao], +) +final class SharedDatabase extends _$SharedDatabase { + SharedDatabase._([QueryExecutor? executor]) + : super(executor ?? _openConnection()); @override - Set get primaryKey => {orderId}; -} - -class ShopinBitSettings extends Table { - // Single row table - always row 0 - IntColumn get id => integer().withDefault(const Constant(0))(); - - BoolColumn get guidelinesAccepted => - boolean().withDefault(const Constant(false))(); - BoolColumn get setupComplete => - boolean().withDefault(const Constant(false))(); - TextColumn get displayName => text().nullable()(); + int get schemaVersion => 2; @override - Set get primaryKey => {id}; + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from == 1 && to == 2) { + await m.createTable(shopinBitSettings); + await m.createTable(shopInBitTickets); + } + }, + ); + + static QueryExecutor _openConnection() { + return driftDatabase( + name: "shared", + native: DriftNativeOptions( + shareAcrossIsolates: true, + databasePath: () async { + final dir = await StackFileSystem.applicationDriftDirectory(); + return path.join(dir.path, "shared", "shared.db"); + }, + ), + ); + } } @DriftAccessor(tables: [ShopinBitSettings]) @@ -74,37 +93,3 @@ class ShopinBitSettingsDao extends DatabaseAccessor )..where((t) => t.id.equals(0))).write(changes); } } - -@DriftDatabase( - tables: [CakepayOrders, ShopinBitSettings], - daos: [ShopinBitSettingsDao], -) -final class SharedDatabase extends _$SharedDatabase { - SharedDatabase._([QueryExecutor? executor]) - : super(executor ?? _openConnection()); - - @override - int get schemaVersion => 2; - - @override - MigrationStrategy get migration => MigrationStrategy( - onUpgrade: (m, from, to) async { - if (from == 1 && to == 2) { - await m.createTable(shopinBitSettings); - } - }, - ); - - static QueryExecutor _openConnection() { - return driftDatabase( - name: "shared", - native: DriftNativeOptions( - shareAcrossIsolates: true, - databasePath: () async { - final dir = await StackFileSystem.applicationDriftDirectory(); - return path.join(dir.path, "shared", "shared.db"); - }, - ), - ); - } -} diff --git a/lib/db/drift/shared_db/shared_database.g.dart b/lib/db/drift/shared_db/shared_database.g.dart new file mode 100644 index 000000000..24a3c8351 --- /dev/null +++ b/lib/db/drift/shared_db/shared_database.g.dart @@ -0,0 +1,2854 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shared_database.dart'; + +// ignore_for_file: type=lint +class $CakepayOrdersTable extends CakepayOrders + with TableInfo<$CakepayOrdersTable, CakepayOrder> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CakepayOrdersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _orderIdMeta = const VerificationMeta( + 'orderId', + ); + @override + late final GeneratedColumn orderId = GeneratedColumn( + 'order_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [orderId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'cakepay_orders'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('order_id')) { + context.handle( + _orderIdMeta, + orderId.isAcceptableOrUnknown(data['order_id']!, _orderIdMeta), + ); + } else if (isInserting) { + context.missing(_orderIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {orderId}; + @override + CakepayOrder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CakepayOrder( + orderId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_id'], + )!, + ); + } + + @override + $CakepayOrdersTable createAlias(String alias) { + return $CakepayOrdersTable(attachedDatabase, alias); + } +} + +class CakepayOrder extends DataClass implements Insertable { + final String orderId; + const CakepayOrder({required this.orderId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['order_id'] = Variable(orderId); + return map; + } + + CakepayOrdersCompanion toCompanion(bool nullToAbsent) { + return CakepayOrdersCompanion(orderId: Value(orderId)); + } + + factory CakepayOrder.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CakepayOrder(orderId: serializer.fromJson(json['orderId'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'orderId': serializer.toJson(orderId)}; + } + + CakepayOrder copyWith({String? orderId}) => + CakepayOrder(orderId: orderId ?? this.orderId); + CakepayOrder copyWithCompanion(CakepayOrdersCompanion data) { + return CakepayOrder( + orderId: data.orderId.present ? data.orderId.value : this.orderId, + ); + } + + @override + String toString() { + return (StringBuffer('CakepayOrder(') + ..write('orderId: $orderId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => orderId.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CakepayOrder && other.orderId == this.orderId); +} + +class CakepayOrdersCompanion extends UpdateCompanion { + final Value orderId; + final Value rowid; + const CakepayOrdersCompanion({ + this.orderId = const Value.absent(), + this.rowid = const Value.absent(), + }); + CakepayOrdersCompanion.insert({ + required String orderId, + this.rowid = const Value.absent(), + }) : orderId = Value(orderId); + static Insertable custom({ + Expression? orderId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (orderId != null) 'order_id': orderId, + if (rowid != null) 'rowid': rowid, + }); + } + + CakepayOrdersCompanion copyWith({Value? orderId, Value? rowid}) { + return CakepayOrdersCompanion( + orderId: orderId ?? this.orderId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (orderId.present) { + map['order_id'] = Variable(orderId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CakepayOrdersCompanion(') + ..write('orderId: $orderId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ShopinBitSettingsTable extends ShopinBitSettings + with TableInfo<$ShopinBitSettingsTable, ShopinBitSetting> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShopinBitSettingsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _guidelinesAcceptedMeta = + const VerificationMeta('guidelinesAccepted'); + @override + late final GeneratedColumn guidelinesAccepted = GeneratedColumn( + 'guidelines_accepted', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("guidelines_accepted" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _setupCompleteMeta = const VerificationMeta( + 'setupComplete', + ); + @override + late final GeneratedColumn setupComplete = GeneratedColumn( + 'setup_complete', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("setup_complete" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + guidelinesAccepted, + setupComplete, + displayName, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shopin_bit_settings'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('guidelines_accepted')) { + context.handle( + _guidelinesAcceptedMeta, + guidelinesAccepted.isAcceptableOrUnknown( + data['guidelines_accepted']!, + _guidelinesAcceptedMeta, + ), + ); + } + if (data.containsKey('setup_complete')) { + context.handle( + _setupCompleteMeta, + setupComplete.isAcceptableOrUnknown( + data['setup_complete']!, + _setupCompleteMeta, + ), + ); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ShopinBitSetting map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShopinBitSetting( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + guidelinesAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}guidelines_accepted'], + )!, + setupComplete: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}setup_complete'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + ), + ); + } + + @override + $ShopinBitSettingsTable createAlias(String alias) { + return $ShopinBitSettingsTable(attachedDatabase, alias); + } +} + +class ShopinBitSetting extends DataClass + implements Insertable { + final int id; + final bool guidelinesAccepted; + final bool setupComplete; + final String? displayName; + const ShopinBitSetting({ + required this.id, + required this.guidelinesAccepted, + required this.setupComplete, + this.displayName, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['guidelines_accepted'] = Variable(guidelinesAccepted); + map['setup_complete'] = Variable(setupComplete); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + return map; + } + + ShopinBitSettingsCompanion toCompanion(bool nullToAbsent) { + return ShopinBitSettingsCompanion( + id: Value(id), + guidelinesAccepted: Value(guidelinesAccepted), + setupComplete: Value(setupComplete), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + ); + } + + factory ShopinBitSetting.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShopinBitSetting( + id: serializer.fromJson(json['id']), + guidelinesAccepted: serializer.fromJson(json['guidelinesAccepted']), + setupComplete: serializer.fromJson(json['setupComplete']), + displayName: serializer.fromJson(json['displayName']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'guidelinesAccepted': serializer.toJson(guidelinesAccepted), + 'setupComplete': serializer.toJson(setupComplete), + 'displayName': serializer.toJson(displayName), + }; + } + + ShopinBitSetting copyWith({ + int? id, + bool? guidelinesAccepted, + bool? setupComplete, + Value displayName = const Value.absent(), + }) => ShopinBitSetting( + id: id ?? this.id, + guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, + setupComplete: setupComplete ?? this.setupComplete, + displayName: displayName.present ? displayName.value : this.displayName, + ); + ShopinBitSetting copyWithCompanion(ShopinBitSettingsCompanion data) { + return ShopinBitSetting( + id: data.id.present ? data.id.value : this.id, + guidelinesAccepted: data.guidelinesAccepted.present + ? data.guidelinesAccepted.value + : this.guidelinesAccepted, + setupComplete: data.setupComplete.present + ? data.setupComplete.value + : this.setupComplete, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + ); + } + + @override + String toString() { + return (StringBuffer('ShopinBitSetting(') + ..write('id: $id, ') + ..write('guidelinesAccepted: $guidelinesAccepted, ') + ..write('setupComplete: $setupComplete, ') + ..write('displayName: $displayName') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, guidelinesAccepted, setupComplete, displayName); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShopinBitSetting && + other.id == this.id && + other.guidelinesAccepted == this.guidelinesAccepted && + other.setupComplete == this.setupComplete && + other.displayName == this.displayName); +} + +class ShopinBitSettingsCompanion extends UpdateCompanion { + final Value id; + final Value guidelinesAccepted; + final Value setupComplete; + final Value displayName; + const ShopinBitSettingsCompanion({ + this.id = const Value.absent(), + this.guidelinesAccepted = const Value.absent(), + this.setupComplete = const Value.absent(), + this.displayName = const Value.absent(), + }); + ShopinBitSettingsCompanion.insert({ + this.id = const Value.absent(), + this.guidelinesAccepted = const Value.absent(), + this.setupComplete = const Value.absent(), + this.displayName = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? guidelinesAccepted, + Expression? setupComplete, + Expression? displayName, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (guidelinesAccepted != null) 'guidelines_accepted': guidelinesAccepted, + if (setupComplete != null) 'setup_complete': setupComplete, + if (displayName != null) 'display_name': displayName, + }); + } + + ShopinBitSettingsCompanion copyWith({ + Value? id, + Value? guidelinesAccepted, + Value? setupComplete, + Value? displayName, + }) { + return ShopinBitSettingsCompanion( + id: id ?? this.id, + guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted, + setupComplete: setupComplete ?? this.setupComplete, + displayName: displayName ?? this.displayName, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (guidelinesAccepted.present) { + map['guidelines_accepted'] = Variable(guidelinesAccepted.value); + } + if (setupComplete.present) { + map['setup_complete'] = Variable(setupComplete.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShopinBitSettingsCompanion(') + ..write('id: $id, ') + ..write('guidelinesAccepted: $guidelinesAccepted, ') + ..write('setupComplete: $setupComplete, ') + ..write('displayName: $displayName') + ..write(')')) + .toString(); + } +} + +class $ShopInBitTicketsTable extends ShopInBitTickets + with TableInfo<$ShopInBitTicketsTable, ShopInBitTicket> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShopInBitTicketsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _ticketIdMeta = const VerificationMeta( + 'ticketId', + ); + @override + late final GeneratedColumn ticketId = GeneratedColumn( + 'ticket_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + late final GeneratedColumnWithTypeConverter category = + GeneratedColumn( + 'category', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter( + $ShopInBitTicketsTable.$convertercategory, + ); + @override + late final GeneratedColumnWithTypeConverter + status = + GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter( + $ShopInBitTicketsTable.$converterstatus, + ); + static const VerificationMeta _requestDescriptionMeta = + const VerificationMeta('requestDescription'); + @override + late final GeneratedColumn requestDescription = + GeneratedColumn( + 'request_description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _deliveryCountryMeta = const VerificationMeta( + 'deliveryCountry', + ); + @override + late final GeneratedColumn deliveryCountry = GeneratedColumn( + 'delivery_country', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _offerProductNameMeta = const VerificationMeta( + 'offerProductName', + ); + @override + late final GeneratedColumn offerProductName = GeneratedColumn( + 'offer_product_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _offerPriceMeta = const VerificationMeta( + 'offerPrice', + ); + @override + late final GeneratedColumn offerPrice = GeneratedColumn( + 'offer_price', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _shippingNameMeta = const VerificationMeta( + 'shippingName', + ); + @override + late final GeneratedColumn shippingName = GeneratedColumn( + 'shipping_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _shippingStreetMeta = const VerificationMeta( + 'shippingStreet', + ); + @override + late final GeneratedColumn shippingStreet = GeneratedColumn( + 'shipping_street', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _shippingCityMeta = const VerificationMeta( + 'shippingCity', + ); + @override + late final GeneratedColumn shippingCity = GeneratedColumn( + 'shipping_city', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _shippingPostalCodeMeta = + const VerificationMeta('shippingPostalCode'); + @override + late final GeneratedColumn shippingPostalCode = + GeneratedColumn( + 'shipping_postal_code', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _shippingCountryMeta = const VerificationMeta( + 'shippingCountry', + ); + @override + late final GeneratedColumn shippingCountry = GeneratedColumn( + 'shipping_country', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _paymentMethodMeta = const VerificationMeta( + 'paymentMethod', + ); + @override + late final GeneratedColumn paymentMethod = GeneratedColumn( + 'payment_method', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter< + List, + String + > + messages = + GeneratedColumn( + 'messages', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ).withConverter>( + $ShopInBitTicketsTable.$convertermessages, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _apiTicketIdMeta = const VerificationMeta( + 'apiTicketId', + ); + @override + late final GeneratedColumn apiTicketId = GeneratedColumn( + 'api_ticket_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _carResearchInvoiceIdMeta = + const VerificationMeta('carResearchInvoiceId'); + @override + late final GeneratedColumn carResearchInvoiceId = + GeneratedColumn( + 'car_research_invoice_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _feeTicketNumberMeta = const VerificationMeta( + 'feeTicketNumber', + ); + @override + late final GeneratedColumn feeTicketNumber = GeneratedColumn( + 'fee_ticket_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _needsCreateRequestMeta = + const VerificationMeta('needsCreateRequest'); + @override + late final GeneratedColumn needsCreateRequest = GeneratedColumn( + 'needs_create_request', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("needs_create_request" IN (0, 1))', + ), + ); + static const VerificationMeta _isPendingPaymentMeta = const VerificationMeta( + 'isPendingPayment', + ); + @override + late final GeneratedColumn isPendingPayment = GeneratedColumn( + 'is_pending_payment', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_pending_payment" IN (0, 1))', + ), + ); + static const VerificationMeta _carResearchExpiresAtMeta = + const VerificationMeta('carResearchExpiresAt'); + @override + late final GeneratedColumn carResearchExpiresAt = + GeneratedColumn( + 'car_research_expires_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _carResearchPaymentLinksMeta = + const VerificationMeta('carResearchPaymentLinks'); + @override + late final GeneratedColumn carResearchPaymentLinks = + GeneratedColumn( + 'car_research_payment_links', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + ticketId, + displayName, + category, + status, + requestDescription, + deliveryCountry, + offerProductName, + offerPrice, + shippingName, + shippingStreet, + shippingCity, + shippingPostalCode, + shippingCountry, + paymentMethod, + messages, + createdAt, + apiTicketId, + carResearchInvoiceId, + feeTicketNumber, + needsCreateRequest, + isPendingPayment, + carResearchExpiresAt, + carResearchPaymentLinks, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shop_in_bit_tickets'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('ticket_id')) { + context.handle( + _ticketIdMeta, + ticketId.isAcceptableOrUnknown(data['ticket_id']!, _ticketIdMeta), + ); + } else if (isInserting) { + context.missing(_ticketIdMeta); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_displayNameMeta); + } + if (data.containsKey('request_description')) { + context.handle( + _requestDescriptionMeta, + requestDescription.isAcceptableOrUnknown( + data['request_description']!, + _requestDescriptionMeta, + ), + ); + } else if (isInserting) { + context.missing(_requestDescriptionMeta); + } + if (data.containsKey('delivery_country')) { + context.handle( + _deliveryCountryMeta, + deliveryCountry.isAcceptableOrUnknown( + data['delivery_country']!, + _deliveryCountryMeta, + ), + ); + } else if (isInserting) { + context.missing(_deliveryCountryMeta); + } + if (data.containsKey('offer_product_name')) { + context.handle( + _offerProductNameMeta, + offerProductName.isAcceptableOrUnknown( + data['offer_product_name']!, + _offerProductNameMeta, + ), + ); + } + if (data.containsKey('offer_price')) { + context.handle( + _offerPriceMeta, + offerPrice.isAcceptableOrUnknown(data['offer_price']!, _offerPriceMeta), + ); + } + if (data.containsKey('shipping_name')) { + context.handle( + _shippingNameMeta, + shippingName.isAcceptableOrUnknown( + data['shipping_name']!, + _shippingNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_shippingNameMeta); + } + if (data.containsKey('shipping_street')) { + context.handle( + _shippingStreetMeta, + shippingStreet.isAcceptableOrUnknown( + data['shipping_street']!, + _shippingStreetMeta, + ), + ); + } else if (isInserting) { + context.missing(_shippingStreetMeta); + } + if (data.containsKey('shipping_city')) { + context.handle( + _shippingCityMeta, + shippingCity.isAcceptableOrUnknown( + data['shipping_city']!, + _shippingCityMeta, + ), + ); + } else if (isInserting) { + context.missing(_shippingCityMeta); + } + if (data.containsKey('shipping_postal_code')) { + context.handle( + _shippingPostalCodeMeta, + shippingPostalCode.isAcceptableOrUnknown( + data['shipping_postal_code']!, + _shippingPostalCodeMeta, + ), + ); + } else if (isInserting) { + context.missing(_shippingPostalCodeMeta); + } + if (data.containsKey('shipping_country')) { + context.handle( + _shippingCountryMeta, + shippingCountry.isAcceptableOrUnknown( + data['shipping_country']!, + _shippingCountryMeta, + ), + ); + } else if (isInserting) { + context.missing(_shippingCountryMeta); + } + if (data.containsKey('payment_method')) { + context.handle( + _paymentMethodMeta, + paymentMethod.isAcceptableOrUnknown( + data['payment_method']!, + _paymentMethodMeta, + ), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('api_ticket_id')) { + context.handle( + _apiTicketIdMeta, + apiTicketId.isAcceptableOrUnknown( + data['api_ticket_id']!, + _apiTicketIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_apiTicketIdMeta); + } + if (data.containsKey('car_research_invoice_id')) { + context.handle( + _carResearchInvoiceIdMeta, + carResearchInvoiceId.isAcceptableOrUnknown( + data['car_research_invoice_id']!, + _carResearchInvoiceIdMeta, + ), + ); + } + if (data.containsKey('fee_ticket_number')) { + context.handle( + _feeTicketNumberMeta, + feeTicketNumber.isAcceptableOrUnknown( + data['fee_ticket_number']!, + _feeTicketNumberMeta, + ), + ); + } + if (data.containsKey('needs_create_request')) { + context.handle( + _needsCreateRequestMeta, + needsCreateRequest.isAcceptableOrUnknown( + data['needs_create_request']!, + _needsCreateRequestMeta, + ), + ); + } else if (isInserting) { + context.missing(_needsCreateRequestMeta); + } + if (data.containsKey('is_pending_payment')) { + context.handle( + _isPendingPaymentMeta, + isPendingPayment.isAcceptableOrUnknown( + data['is_pending_payment']!, + _isPendingPaymentMeta, + ), + ); + } else if (isInserting) { + context.missing(_isPendingPaymentMeta); + } + if (data.containsKey('car_research_expires_at')) { + context.handle( + _carResearchExpiresAtMeta, + carResearchExpiresAt.isAcceptableOrUnknown( + data['car_research_expires_at']!, + _carResearchExpiresAtMeta, + ), + ); + } + if (data.containsKey('car_research_payment_links')) { + context.handle( + _carResearchPaymentLinksMeta, + carResearchPaymentLinks.isAcceptableOrUnknown( + data['car_research_payment_links']!, + _carResearchPaymentLinksMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {ticketId}; + @override + ShopInBitTicket map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShopInBitTicket( + ticketId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ticket_id'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + category: $ShopInBitTicketsTable.$convertercategory.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}category'], + )!, + ), + status: $ShopInBitTicketsTable.$converterstatus.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}status'], + )!, + ), + requestDescription: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}request_description'], + )!, + deliveryCountry: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}delivery_country'], + )!, + offerProductName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}offer_product_name'], + ), + offerPrice: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}offer_price'], + ), + shippingName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shipping_name'], + )!, + shippingStreet: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shipping_street'], + )!, + shippingCity: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shipping_city'], + )!, + shippingPostalCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shipping_postal_code'], + )!, + shippingCountry: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shipping_country'], + )!, + paymentMethod: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_method'], + ), + messages: $ShopInBitTicketsTable.$convertermessages.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}messages'], + )!, + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + apiTicketId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}api_ticket_id'], + )!, + carResearchInvoiceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}car_research_invoice_id'], + ), + feeTicketNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}fee_ticket_number'], + ), + needsCreateRequest: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}needs_create_request'], + )!, + isPendingPayment: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_pending_payment'], + )!, + carResearchExpiresAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}car_research_expires_at'], + ), + carResearchPaymentLinks: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}car_research_payment_links'], + ), + ); + } + + @override + $ShopInBitTicketsTable createAlias(String alias) { + return $ShopInBitTicketsTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertercategory = + const EnumIndexConverter(ShopInBitCategory.values); + static JsonTypeConverter2 $converterstatus = + const EnumIndexConverter( + ShopInBitOrderStatus.values, + ); + static JsonTypeConverter2, String, List> + $convertermessages = const ShopInBitTicketMessagesConverter(); +} + +class ShopInBitTicket extends DataClass implements Insertable { + final String ticketId; + final String displayName; + final ShopInBitCategory category; + final ShopInBitOrderStatus status; + final String requestDescription; + final String deliveryCountry; + final String? offerProductName; + final String? offerPrice; + final String shippingName; + final String shippingStreet; + final String shippingCity; + final String shippingPostalCode; + final String shippingCountry; + final String? paymentMethod; + final List messages; + final DateTime createdAt; + final int apiTicketId; + final String? carResearchInvoiceId; + final String? feeTicketNumber; + final bool needsCreateRequest; + final bool isPendingPayment; + final DateTime? carResearchExpiresAt; + final String? carResearchPaymentLinks; + const ShopInBitTicket({ + required this.ticketId, + required this.displayName, + required this.category, + required this.status, + required this.requestDescription, + required this.deliveryCountry, + this.offerProductName, + this.offerPrice, + required this.shippingName, + required this.shippingStreet, + required this.shippingCity, + required this.shippingPostalCode, + required this.shippingCountry, + this.paymentMethod, + required this.messages, + required this.createdAt, + required this.apiTicketId, + this.carResearchInvoiceId, + this.feeTicketNumber, + required this.needsCreateRequest, + required this.isPendingPayment, + this.carResearchExpiresAt, + this.carResearchPaymentLinks, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['ticket_id'] = Variable(ticketId); + map['display_name'] = Variable(displayName); + { + map['category'] = Variable( + $ShopInBitTicketsTable.$convertercategory.toSql(category), + ); + } + { + map['status'] = Variable( + $ShopInBitTicketsTable.$converterstatus.toSql(status), + ); + } + map['request_description'] = Variable(requestDescription); + map['delivery_country'] = Variable(deliveryCountry); + if (!nullToAbsent || offerProductName != null) { + map['offer_product_name'] = Variable(offerProductName); + } + if (!nullToAbsent || offerPrice != null) { + map['offer_price'] = Variable(offerPrice); + } + map['shipping_name'] = Variable(shippingName); + map['shipping_street'] = Variable(shippingStreet); + map['shipping_city'] = Variable(shippingCity); + map['shipping_postal_code'] = Variable(shippingPostalCode); + map['shipping_country'] = Variable(shippingCountry); + if (!nullToAbsent || paymentMethod != null) { + map['payment_method'] = Variable(paymentMethod); + } + { + map['messages'] = Variable( + $ShopInBitTicketsTable.$convertermessages.toSql(messages), + ); + } + map['created_at'] = Variable(createdAt); + map['api_ticket_id'] = Variable(apiTicketId); + if (!nullToAbsent || carResearchInvoiceId != null) { + map['car_research_invoice_id'] = Variable(carResearchInvoiceId); + } + if (!nullToAbsent || feeTicketNumber != null) { + map['fee_ticket_number'] = Variable(feeTicketNumber); + } + map['needs_create_request'] = Variable(needsCreateRequest); + map['is_pending_payment'] = Variable(isPendingPayment); + if (!nullToAbsent || carResearchExpiresAt != null) { + map['car_research_expires_at'] = Variable(carResearchExpiresAt); + } + if (!nullToAbsent || carResearchPaymentLinks != null) { + map['car_research_payment_links'] = Variable( + carResearchPaymentLinks, + ); + } + return map; + } + + ShopInBitTicketsCompanion toCompanion(bool nullToAbsent) { + return ShopInBitTicketsCompanion( + ticketId: Value(ticketId), + displayName: Value(displayName), + category: Value(category), + status: Value(status), + requestDescription: Value(requestDescription), + deliveryCountry: Value(deliveryCountry), + offerProductName: offerProductName == null && nullToAbsent + ? const Value.absent() + : Value(offerProductName), + offerPrice: offerPrice == null && nullToAbsent + ? const Value.absent() + : Value(offerPrice), + shippingName: Value(shippingName), + shippingStreet: Value(shippingStreet), + shippingCity: Value(shippingCity), + shippingPostalCode: Value(shippingPostalCode), + shippingCountry: Value(shippingCountry), + paymentMethod: paymentMethod == null && nullToAbsent + ? const Value.absent() + : Value(paymentMethod), + messages: Value(messages), + createdAt: Value(createdAt), + apiTicketId: Value(apiTicketId), + carResearchInvoiceId: carResearchInvoiceId == null && nullToAbsent + ? const Value.absent() + : Value(carResearchInvoiceId), + feeTicketNumber: feeTicketNumber == null && nullToAbsent + ? const Value.absent() + : Value(feeTicketNumber), + needsCreateRequest: Value(needsCreateRequest), + isPendingPayment: Value(isPendingPayment), + carResearchExpiresAt: carResearchExpiresAt == null && nullToAbsent + ? const Value.absent() + : Value(carResearchExpiresAt), + carResearchPaymentLinks: carResearchPaymentLinks == null && nullToAbsent + ? const Value.absent() + : Value(carResearchPaymentLinks), + ); + } + + factory ShopInBitTicket.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShopInBitTicket( + ticketId: serializer.fromJson(json['ticketId']), + displayName: serializer.fromJson(json['displayName']), + category: $ShopInBitTicketsTable.$convertercategory.fromJson( + serializer.fromJson(json['category']), + ), + status: $ShopInBitTicketsTable.$converterstatus.fromJson( + serializer.fromJson(json['status']), + ), + requestDescription: serializer.fromJson( + json['requestDescription'], + ), + deliveryCountry: serializer.fromJson(json['deliveryCountry']), + offerProductName: serializer.fromJson(json['offerProductName']), + offerPrice: serializer.fromJson(json['offerPrice']), + shippingName: serializer.fromJson(json['shippingName']), + shippingStreet: serializer.fromJson(json['shippingStreet']), + shippingCity: serializer.fromJson(json['shippingCity']), + shippingPostalCode: serializer.fromJson( + json['shippingPostalCode'], + ), + shippingCountry: serializer.fromJson(json['shippingCountry']), + paymentMethod: serializer.fromJson(json['paymentMethod']), + messages: $ShopInBitTicketsTable.$convertermessages.fromJson( + serializer.fromJson>(json['messages']), + ), + createdAt: serializer.fromJson(json['createdAt']), + apiTicketId: serializer.fromJson(json['apiTicketId']), + carResearchInvoiceId: serializer.fromJson( + json['carResearchInvoiceId'], + ), + feeTicketNumber: serializer.fromJson(json['feeTicketNumber']), + needsCreateRequest: serializer.fromJson(json['needsCreateRequest']), + isPendingPayment: serializer.fromJson(json['isPendingPayment']), + carResearchExpiresAt: serializer.fromJson( + json['carResearchExpiresAt'], + ), + carResearchPaymentLinks: serializer.fromJson( + json['carResearchPaymentLinks'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'ticketId': serializer.toJson(ticketId), + 'displayName': serializer.toJson(displayName), + 'category': serializer.toJson( + $ShopInBitTicketsTable.$convertercategory.toJson(category), + ), + 'status': serializer.toJson( + $ShopInBitTicketsTable.$converterstatus.toJson(status), + ), + 'requestDescription': serializer.toJson(requestDescription), + 'deliveryCountry': serializer.toJson(deliveryCountry), + 'offerProductName': serializer.toJson(offerProductName), + 'offerPrice': serializer.toJson(offerPrice), + 'shippingName': serializer.toJson(shippingName), + 'shippingStreet': serializer.toJson(shippingStreet), + 'shippingCity': serializer.toJson(shippingCity), + 'shippingPostalCode': serializer.toJson(shippingPostalCode), + 'shippingCountry': serializer.toJson(shippingCountry), + 'paymentMethod': serializer.toJson(paymentMethod), + 'messages': serializer.toJson>( + $ShopInBitTicketsTable.$convertermessages.toJson(messages), + ), + 'createdAt': serializer.toJson(createdAt), + 'apiTicketId': serializer.toJson(apiTicketId), + 'carResearchInvoiceId': serializer.toJson(carResearchInvoiceId), + 'feeTicketNumber': serializer.toJson(feeTicketNumber), + 'needsCreateRequest': serializer.toJson(needsCreateRequest), + 'isPendingPayment': serializer.toJson(isPendingPayment), + 'carResearchExpiresAt': serializer.toJson( + carResearchExpiresAt, + ), + 'carResearchPaymentLinks': serializer.toJson( + carResearchPaymentLinks, + ), + }; + } + + ShopInBitTicket copyWith({ + String? ticketId, + String? displayName, + ShopInBitCategory? category, + ShopInBitOrderStatus? status, + String? requestDescription, + String? deliveryCountry, + Value offerProductName = const Value.absent(), + Value offerPrice = const Value.absent(), + String? shippingName, + String? shippingStreet, + String? shippingCity, + String? shippingPostalCode, + String? shippingCountry, + Value paymentMethod = const Value.absent(), + List? messages, + DateTime? createdAt, + int? apiTicketId, + Value carResearchInvoiceId = const Value.absent(), + Value feeTicketNumber = const Value.absent(), + bool? needsCreateRequest, + bool? isPendingPayment, + Value carResearchExpiresAt = const Value.absent(), + Value carResearchPaymentLinks = const Value.absent(), + }) => ShopInBitTicket( + ticketId: ticketId ?? this.ticketId, + displayName: displayName ?? this.displayName, + category: category ?? this.category, + status: status ?? this.status, + requestDescription: requestDescription ?? this.requestDescription, + deliveryCountry: deliveryCountry ?? this.deliveryCountry, + offerProductName: offerProductName.present + ? offerProductName.value + : this.offerProductName, + offerPrice: offerPrice.present ? offerPrice.value : this.offerPrice, + shippingName: shippingName ?? this.shippingName, + shippingStreet: shippingStreet ?? this.shippingStreet, + shippingCity: shippingCity ?? this.shippingCity, + shippingPostalCode: shippingPostalCode ?? this.shippingPostalCode, + shippingCountry: shippingCountry ?? this.shippingCountry, + paymentMethod: paymentMethod.present + ? paymentMethod.value + : this.paymentMethod, + messages: messages ?? this.messages, + createdAt: createdAt ?? this.createdAt, + apiTicketId: apiTicketId ?? this.apiTicketId, + carResearchInvoiceId: carResearchInvoiceId.present + ? carResearchInvoiceId.value + : this.carResearchInvoiceId, + feeTicketNumber: feeTicketNumber.present + ? feeTicketNumber.value + : this.feeTicketNumber, + needsCreateRequest: needsCreateRequest ?? this.needsCreateRequest, + isPendingPayment: isPendingPayment ?? this.isPendingPayment, + carResearchExpiresAt: carResearchExpiresAt.present + ? carResearchExpiresAt.value + : this.carResearchExpiresAt, + carResearchPaymentLinks: carResearchPaymentLinks.present + ? carResearchPaymentLinks.value + : this.carResearchPaymentLinks, + ); + ShopInBitTicket copyWithCompanion(ShopInBitTicketsCompanion data) { + return ShopInBitTicket( + ticketId: data.ticketId.present ? data.ticketId.value : this.ticketId, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + category: data.category.present ? data.category.value : this.category, + status: data.status.present ? data.status.value : this.status, + requestDescription: data.requestDescription.present + ? data.requestDescription.value + : this.requestDescription, + deliveryCountry: data.deliveryCountry.present + ? data.deliveryCountry.value + : this.deliveryCountry, + offerProductName: data.offerProductName.present + ? data.offerProductName.value + : this.offerProductName, + offerPrice: data.offerPrice.present + ? data.offerPrice.value + : this.offerPrice, + shippingName: data.shippingName.present + ? data.shippingName.value + : this.shippingName, + shippingStreet: data.shippingStreet.present + ? data.shippingStreet.value + : this.shippingStreet, + shippingCity: data.shippingCity.present + ? data.shippingCity.value + : this.shippingCity, + shippingPostalCode: data.shippingPostalCode.present + ? data.shippingPostalCode.value + : this.shippingPostalCode, + shippingCountry: data.shippingCountry.present + ? data.shippingCountry.value + : this.shippingCountry, + paymentMethod: data.paymentMethod.present + ? data.paymentMethod.value + : this.paymentMethod, + messages: data.messages.present ? data.messages.value : this.messages, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + apiTicketId: data.apiTicketId.present + ? data.apiTicketId.value + : this.apiTicketId, + carResearchInvoiceId: data.carResearchInvoiceId.present + ? data.carResearchInvoiceId.value + : this.carResearchInvoiceId, + feeTicketNumber: data.feeTicketNumber.present + ? data.feeTicketNumber.value + : this.feeTicketNumber, + needsCreateRequest: data.needsCreateRequest.present + ? data.needsCreateRequest.value + : this.needsCreateRequest, + isPendingPayment: data.isPendingPayment.present + ? data.isPendingPayment.value + : this.isPendingPayment, + carResearchExpiresAt: data.carResearchExpiresAt.present + ? data.carResearchExpiresAt.value + : this.carResearchExpiresAt, + carResearchPaymentLinks: data.carResearchPaymentLinks.present + ? data.carResearchPaymentLinks.value + : this.carResearchPaymentLinks, + ); + } + + @override + String toString() { + return (StringBuffer('ShopInBitTicket(') + ..write('ticketId: $ticketId, ') + ..write('displayName: $displayName, ') + ..write('category: $category, ') + ..write('status: $status, ') + ..write('requestDescription: $requestDescription, ') + ..write('deliveryCountry: $deliveryCountry, ') + ..write('offerProductName: $offerProductName, ') + ..write('offerPrice: $offerPrice, ') + ..write('shippingName: $shippingName, ') + ..write('shippingStreet: $shippingStreet, ') + ..write('shippingCity: $shippingCity, ') + ..write('shippingPostalCode: $shippingPostalCode, ') + ..write('shippingCountry: $shippingCountry, ') + ..write('paymentMethod: $paymentMethod, ') + ..write('messages: $messages, ') + ..write('createdAt: $createdAt, ') + ..write('apiTicketId: $apiTicketId, ') + ..write('carResearchInvoiceId: $carResearchInvoiceId, ') + ..write('feeTicketNumber: $feeTicketNumber, ') + ..write('needsCreateRequest: $needsCreateRequest, ') + ..write('isPendingPayment: $isPendingPayment, ') + ..write('carResearchExpiresAt: $carResearchExpiresAt, ') + ..write('carResearchPaymentLinks: $carResearchPaymentLinks') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + ticketId, + displayName, + category, + status, + requestDescription, + deliveryCountry, + offerProductName, + offerPrice, + shippingName, + shippingStreet, + shippingCity, + shippingPostalCode, + shippingCountry, + paymentMethod, + messages, + createdAt, + apiTicketId, + carResearchInvoiceId, + feeTicketNumber, + needsCreateRequest, + isPendingPayment, + carResearchExpiresAt, + carResearchPaymentLinks, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShopInBitTicket && + other.ticketId == this.ticketId && + other.displayName == this.displayName && + other.category == this.category && + other.status == this.status && + other.requestDescription == this.requestDescription && + other.deliveryCountry == this.deliveryCountry && + other.offerProductName == this.offerProductName && + other.offerPrice == this.offerPrice && + other.shippingName == this.shippingName && + other.shippingStreet == this.shippingStreet && + other.shippingCity == this.shippingCity && + other.shippingPostalCode == this.shippingPostalCode && + other.shippingCountry == this.shippingCountry && + other.paymentMethod == this.paymentMethod && + other.messages == this.messages && + other.createdAt == this.createdAt && + other.apiTicketId == this.apiTicketId && + other.carResearchInvoiceId == this.carResearchInvoiceId && + other.feeTicketNumber == this.feeTicketNumber && + other.needsCreateRequest == this.needsCreateRequest && + other.isPendingPayment == this.isPendingPayment && + other.carResearchExpiresAt == this.carResearchExpiresAt && + other.carResearchPaymentLinks == this.carResearchPaymentLinks); +} + +class ShopInBitTicketsCompanion extends UpdateCompanion { + final Value ticketId; + final Value displayName; + final Value category; + final Value status; + final Value requestDescription; + final Value deliveryCountry; + final Value offerProductName; + final Value offerPrice; + final Value shippingName; + final Value shippingStreet; + final Value shippingCity; + final Value shippingPostalCode; + final Value shippingCountry; + final Value paymentMethod; + final Value> messages; + final Value createdAt; + final Value apiTicketId; + final Value carResearchInvoiceId; + final Value feeTicketNumber; + final Value needsCreateRequest; + final Value isPendingPayment; + final Value carResearchExpiresAt; + final Value carResearchPaymentLinks; + final Value rowid; + const ShopInBitTicketsCompanion({ + this.ticketId = const Value.absent(), + this.displayName = const Value.absent(), + this.category = const Value.absent(), + this.status = const Value.absent(), + this.requestDescription = const Value.absent(), + this.deliveryCountry = const Value.absent(), + this.offerProductName = const Value.absent(), + this.offerPrice = const Value.absent(), + this.shippingName = const Value.absent(), + this.shippingStreet = const Value.absent(), + this.shippingCity = const Value.absent(), + this.shippingPostalCode = const Value.absent(), + this.shippingCountry = const Value.absent(), + this.paymentMethod = const Value.absent(), + this.messages = const Value.absent(), + this.createdAt = const Value.absent(), + this.apiTicketId = const Value.absent(), + this.carResearchInvoiceId = const Value.absent(), + this.feeTicketNumber = const Value.absent(), + this.needsCreateRequest = const Value.absent(), + this.isPendingPayment = const Value.absent(), + this.carResearchExpiresAt = const Value.absent(), + this.carResearchPaymentLinks = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShopInBitTicketsCompanion.insert({ + required String ticketId, + required String displayName, + required ShopInBitCategory category, + required ShopInBitOrderStatus status, + required String requestDescription, + required String deliveryCountry, + this.offerProductName = const Value.absent(), + this.offerPrice = const Value.absent(), + required String shippingName, + required String shippingStreet, + required String shippingCity, + required String shippingPostalCode, + required String shippingCountry, + this.paymentMethod = const Value.absent(), + required List messages, + required DateTime createdAt, + required int apiTicketId, + this.carResearchInvoiceId = const Value.absent(), + this.feeTicketNumber = const Value.absent(), + required bool needsCreateRequest, + required bool isPendingPayment, + this.carResearchExpiresAt = const Value.absent(), + this.carResearchPaymentLinks = const Value.absent(), + this.rowid = const Value.absent(), + }) : ticketId = Value(ticketId), + displayName = Value(displayName), + category = Value(category), + status = Value(status), + requestDescription = Value(requestDescription), + deliveryCountry = Value(deliveryCountry), + shippingName = Value(shippingName), + shippingStreet = Value(shippingStreet), + shippingCity = Value(shippingCity), + shippingPostalCode = Value(shippingPostalCode), + shippingCountry = Value(shippingCountry), + messages = Value(messages), + createdAt = Value(createdAt), + apiTicketId = Value(apiTicketId), + needsCreateRequest = Value(needsCreateRequest), + isPendingPayment = Value(isPendingPayment); + static Insertable custom({ + Expression? ticketId, + Expression? displayName, + Expression? category, + Expression? status, + Expression? requestDescription, + Expression? deliveryCountry, + Expression? offerProductName, + Expression? offerPrice, + Expression? shippingName, + Expression? shippingStreet, + Expression? shippingCity, + Expression? shippingPostalCode, + Expression? shippingCountry, + Expression? paymentMethod, + Expression? messages, + Expression? createdAt, + Expression? apiTicketId, + Expression? carResearchInvoiceId, + Expression? feeTicketNumber, + Expression? needsCreateRequest, + Expression? isPendingPayment, + Expression? carResearchExpiresAt, + Expression? carResearchPaymentLinks, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (ticketId != null) 'ticket_id': ticketId, + if (displayName != null) 'display_name': displayName, + if (category != null) 'category': category, + if (status != null) 'status': status, + if (requestDescription != null) 'request_description': requestDescription, + if (deliveryCountry != null) 'delivery_country': deliveryCountry, + if (offerProductName != null) 'offer_product_name': offerProductName, + if (offerPrice != null) 'offer_price': offerPrice, + if (shippingName != null) 'shipping_name': shippingName, + if (shippingStreet != null) 'shipping_street': shippingStreet, + if (shippingCity != null) 'shipping_city': shippingCity, + if (shippingPostalCode != null) + 'shipping_postal_code': shippingPostalCode, + if (shippingCountry != null) 'shipping_country': shippingCountry, + if (paymentMethod != null) 'payment_method': paymentMethod, + if (messages != null) 'messages': messages, + if (createdAt != null) 'created_at': createdAt, + if (apiTicketId != null) 'api_ticket_id': apiTicketId, + if (carResearchInvoiceId != null) + 'car_research_invoice_id': carResearchInvoiceId, + if (feeTicketNumber != null) 'fee_ticket_number': feeTicketNumber, + if (needsCreateRequest != null) + 'needs_create_request': needsCreateRequest, + if (isPendingPayment != null) 'is_pending_payment': isPendingPayment, + if (carResearchExpiresAt != null) + 'car_research_expires_at': carResearchExpiresAt, + if (carResearchPaymentLinks != null) + 'car_research_payment_links': carResearchPaymentLinks, + if (rowid != null) 'rowid': rowid, + }); + } + + ShopInBitTicketsCompanion copyWith({ + Value? ticketId, + Value? displayName, + Value? category, + Value? status, + Value? requestDescription, + Value? deliveryCountry, + Value? offerProductName, + Value? offerPrice, + Value? shippingName, + Value? shippingStreet, + Value? shippingCity, + Value? shippingPostalCode, + Value? shippingCountry, + Value? paymentMethod, + Value>? messages, + Value? createdAt, + Value? apiTicketId, + Value? carResearchInvoiceId, + Value? feeTicketNumber, + Value? needsCreateRequest, + Value? isPendingPayment, + Value? carResearchExpiresAt, + Value? carResearchPaymentLinks, + Value? rowid, + }) { + return ShopInBitTicketsCompanion( + ticketId: ticketId ?? this.ticketId, + displayName: displayName ?? this.displayName, + category: category ?? this.category, + status: status ?? this.status, + requestDescription: requestDescription ?? this.requestDescription, + deliveryCountry: deliveryCountry ?? this.deliveryCountry, + offerProductName: offerProductName ?? this.offerProductName, + offerPrice: offerPrice ?? this.offerPrice, + shippingName: shippingName ?? this.shippingName, + shippingStreet: shippingStreet ?? this.shippingStreet, + shippingCity: shippingCity ?? this.shippingCity, + shippingPostalCode: shippingPostalCode ?? this.shippingPostalCode, + shippingCountry: shippingCountry ?? this.shippingCountry, + paymentMethod: paymentMethod ?? this.paymentMethod, + messages: messages ?? this.messages, + createdAt: createdAt ?? this.createdAt, + apiTicketId: apiTicketId ?? this.apiTicketId, + carResearchInvoiceId: carResearchInvoiceId ?? this.carResearchInvoiceId, + feeTicketNumber: feeTicketNumber ?? this.feeTicketNumber, + needsCreateRequest: needsCreateRequest ?? this.needsCreateRequest, + isPendingPayment: isPendingPayment ?? this.isPendingPayment, + carResearchExpiresAt: carResearchExpiresAt ?? this.carResearchExpiresAt, + carResearchPaymentLinks: + carResearchPaymentLinks ?? this.carResearchPaymentLinks, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (ticketId.present) { + map['ticket_id'] = Variable(ticketId.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (category.present) { + map['category'] = Variable( + $ShopInBitTicketsTable.$convertercategory.toSql(category.value), + ); + } + if (status.present) { + map['status'] = Variable( + $ShopInBitTicketsTable.$converterstatus.toSql(status.value), + ); + } + if (requestDescription.present) { + map['request_description'] = Variable(requestDescription.value); + } + if (deliveryCountry.present) { + map['delivery_country'] = Variable(deliveryCountry.value); + } + if (offerProductName.present) { + map['offer_product_name'] = Variable(offerProductName.value); + } + if (offerPrice.present) { + map['offer_price'] = Variable(offerPrice.value); + } + if (shippingName.present) { + map['shipping_name'] = Variable(shippingName.value); + } + if (shippingStreet.present) { + map['shipping_street'] = Variable(shippingStreet.value); + } + if (shippingCity.present) { + map['shipping_city'] = Variable(shippingCity.value); + } + if (shippingPostalCode.present) { + map['shipping_postal_code'] = Variable(shippingPostalCode.value); + } + if (shippingCountry.present) { + map['shipping_country'] = Variable(shippingCountry.value); + } + if (paymentMethod.present) { + map['payment_method'] = Variable(paymentMethod.value); + } + if (messages.present) { + map['messages'] = Variable( + $ShopInBitTicketsTable.$convertermessages.toSql(messages.value), + ); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (apiTicketId.present) { + map['api_ticket_id'] = Variable(apiTicketId.value); + } + if (carResearchInvoiceId.present) { + map['car_research_invoice_id'] = Variable( + carResearchInvoiceId.value, + ); + } + if (feeTicketNumber.present) { + map['fee_ticket_number'] = Variable(feeTicketNumber.value); + } + if (needsCreateRequest.present) { + map['needs_create_request'] = Variable(needsCreateRequest.value); + } + if (isPendingPayment.present) { + map['is_pending_payment'] = Variable(isPendingPayment.value); + } + if (carResearchExpiresAt.present) { + map['car_research_expires_at'] = Variable( + carResearchExpiresAt.value, + ); + } + if (carResearchPaymentLinks.present) { + map['car_research_payment_links'] = Variable( + carResearchPaymentLinks.value, + ); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShopInBitTicketsCompanion(') + ..write('ticketId: $ticketId, ') + ..write('displayName: $displayName, ') + ..write('category: $category, ') + ..write('status: $status, ') + ..write('requestDescription: $requestDescription, ') + ..write('deliveryCountry: $deliveryCountry, ') + ..write('offerProductName: $offerProductName, ') + ..write('offerPrice: $offerPrice, ') + ..write('shippingName: $shippingName, ') + ..write('shippingStreet: $shippingStreet, ') + ..write('shippingCity: $shippingCity, ') + ..write('shippingPostalCode: $shippingPostalCode, ') + ..write('shippingCountry: $shippingCountry, ') + ..write('paymentMethod: $paymentMethod, ') + ..write('messages: $messages, ') + ..write('createdAt: $createdAt, ') + ..write('apiTicketId: $apiTicketId, ') + ..write('carResearchInvoiceId: $carResearchInvoiceId, ') + ..write('feeTicketNumber: $feeTicketNumber, ') + ..write('needsCreateRequest: $needsCreateRequest, ') + ..write('isPendingPayment: $isPendingPayment, ') + ..write('carResearchExpiresAt: $carResearchExpiresAt, ') + ..write('carResearchPaymentLinks: $carResearchPaymentLinks, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$SharedDatabase extends GeneratedDatabase { + _$SharedDatabase(QueryExecutor e) : super(e); + $SharedDatabaseManager get managers => $SharedDatabaseManager(this); + late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this); + late final $ShopinBitSettingsTable shopinBitSettings = + $ShopinBitSettingsTable(this); + late final $ShopInBitTicketsTable shopInBitTickets = $ShopInBitTicketsTable( + this, + ); + late final ShopinBitSettingsDao shopinBitSettingsDao = ShopinBitSettingsDao( + this as SharedDatabase, + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + cakepayOrders, + shopinBitSettings, + shopInBitTickets, + ]; +} + +typedef $$CakepayOrdersTableCreateCompanionBuilder = + CakepayOrdersCompanion Function({ + required String orderId, + Value rowid, + }); +typedef $$CakepayOrdersTableUpdateCompanionBuilder = + CakepayOrdersCompanion Function({Value orderId, Value rowid}); + +class $$CakepayOrdersTableFilterComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CakepayOrdersTableOrderingComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CakepayOrdersTableAnnotationComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get orderId => + $composableBuilder(column: $table.orderId, builder: (column) => column); +} + +class $$CakepayOrdersTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + > { + $$CakepayOrdersTableTableManager( + _$SharedDatabase db, + $CakepayOrdersTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CakepayOrdersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CakepayOrdersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CakepayOrdersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value orderId = const Value.absent(), + Value rowid = const Value.absent(), + }) => CakepayOrdersCompanion(orderId: orderId, rowid: rowid), + createCompanionCallback: + ({ + required String orderId, + Value rowid = const Value.absent(), + }) => + CakepayOrdersCompanion.insert(orderId: orderId, rowid: rowid), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CakepayOrdersTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + >; +typedef $$ShopinBitSettingsTableCreateCompanionBuilder = + ShopinBitSettingsCompanion Function({ + Value id, + Value guidelinesAccepted, + Value setupComplete, + Value displayName, + }); +typedef $$ShopinBitSettingsTableUpdateCompanionBuilder = + ShopinBitSettingsCompanion Function({ + Value id, + Value guidelinesAccepted, + Value setupComplete, + Value displayName, + }); + +class $$ShopinBitSettingsTableFilterComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ShopinBitSettingsTableOrderingComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ShopinBitSettingsTableAnnotationComposer + extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> { + $$ShopinBitSettingsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get guidelinesAccepted => $composableBuilder( + column: $table.guidelinesAccepted, + builder: (column) => column, + ); + + GeneratedColumn get setupComplete => $composableBuilder( + column: $table.setupComplete, + builder: (column) => column, + ); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); +} + +class $$ShopinBitSettingsTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting, + $$ShopinBitSettingsTableFilterComposer, + $$ShopinBitSettingsTableOrderingComposer, + $$ShopinBitSettingsTableAnnotationComposer, + $$ShopinBitSettingsTableCreateCompanionBuilder, + $$ShopinBitSettingsTableUpdateCompanionBuilder, + ( + ShopinBitSetting, + BaseReferences< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting + >, + ), + ShopinBitSetting, + PrefetchHooks Function() + > { + $$ShopinBitSettingsTableTableManager( + _$SharedDatabase db, + $ShopinBitSettingsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShopinBitSettingsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShopinBitSettingsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShopinBitSettingsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value guidelinesAccepted = const Value.absent(), + Value setupComplete = const Value.absent(), + Value displayName = const Value.absent(), + }) => ShopinBitSettingsCompanion( + id: id, + guidelinesAccepted: guidelinesAccepted, + setupComplete: setupComplete, + displayName: displayName, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + Value guidelinesAccepted = const Value.absent(), + Value setupComplete = const Value.absent(), + Value displayName = const Value.absent(), + }) => ShopinBitSettingsCompanion.insert( + id: id, + guidelinesAccepted: guidelinesAccepted, + setupComplete: setupComplete, + displayName: displayName, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ShopinBitSettingsTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting, + $$ShopinBitSettingsTableFilterComposer, + $$ShopinBitSettingsTableOrderingComposer, + $$ShopinBitSettingsTableAnnotationComposer, + $$ShopinBitSettingsTableCreateCompanionBuilder, + $$ShopinBitSettingsTableUpdateCompanionBuilder, + ( + ShopinBitSetting, + BaseReferences< + _$SharedDatabase, + $ShopinBitSettingsTable, + ShopinBitSetting + >, + ), + ShopinBitSetting, + PrefetchHooks Function() + >; +typedef $$ShopInBitTicketsTableCreateCompanionBuilder = + ShopInBitTicketsCompanion Function({ + required String ticketId, + required String displayName, + required ShopInBitCategory category, + required ShopInBitOrderStatus status, + required String requestDescription, + required String deliveryCountry, + Value offerProductName, + Value offerPrice, + required String shippingName, + required String shippingStreet, + required String shippingCity, + required String shippingPostalCode, + required String shippingCountry, + Value paymentMethod, + required List messages, + required DateTime createdAt, + required int apiTicketId, + Value carResearchInvoiceId, + Value feeTicketNumber, + required bool needsCreateRequest, + required bool isPendingPayment, + Value carResearchExpiresAt, + Value carResearchPaymentLinks, + Value rowid, + }); +typedef $$ShopInBitTicketsTableUpdateCompanionBuilder = + ShopInBitTicketsCompanion Function({ + Value ticketId, + Value displayName, + Value category, + Value status, + Value requestDescription, + Value deliveryCountry, + Value offerProductName, + Value offerPrice, + Value shippingName, + Value shippingStreet, + Value shippingCity, + Value shippingPostalCode, + Value shippingCountry, + Value paymentMethod, + Value> messages, + Value createdAt, + Value apiTicketId, + Value carResearchInvoiceId, + Value feeTicketNumber, + Value needsCreateRequest, + Value isPendingPayment, + Value carResearchExpiresAt, + Value carResearchPaymentLinks, + Value rowid, + }); + +class $$ShopInBitTicketsTableFilterComposer + extends Composer<_$SharedDatabase, $ShopInBitTicketsTable> { + $$ShopInBitTicketsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get ticketId => $composableBuilder( + column: $table.ticketId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters + get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters< + ShopInBitOrderStatus, + ShopInBitOrderStatus, + int + > + get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get requestDescription => $composableBuilder( + column: $table.requestDescription, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryCountry => $composableBuilder( + column: $table.deliveryCountry, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get offerProductName => $composableBuilder( + column: $table.offerProductName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get offerPrice => $composableBuilder( + column: $table.offerPrice, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingName => $composableBuilder( + column: $table.shippingName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingStreet => $composableBuilder( + column: $table.shippingStreet, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingCity => $composableBuilder( + column: $table.shippingCity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingPostalCode => $composableBuilder( + column: $table.shippingPostalCode, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingCountry => $composableBuilder( + column: $table.shippingCountry, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters< + List, + List, + String + > + get messages => $composableBuilder( + column: $table.messages, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get apiTicketId => $composableBuilder( + column: $table.apiTicketId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get carResearchInvoiceId => $composableBuilder( + column: $table.carResearchInvoiceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get feeTicketNumber => $composableBuilder( + column: $table.feeTicketNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get needsCreateRequest => $composableBuilder( + column: $table.needsCreateRequest, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isPendingPayment => $composableBuilder( + column: $table.isPendingPayment, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get carResearchExpiresAt => $composableBuilder( + column: $table.carResearchExpiresAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get carResearchPaymentLinks => $composableBuilder( + column: $table.carResearchPaymentLinks, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ShopInBitTicketsTableOrderingComposer + extends Composer<_$SharedDatabase, $ShopInBitTicketsTable> { + $$ShopInBitTicketsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get ticketId => $composableBuilder( + column: $table.ticketId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get requestDescription => $composableBuilder( + column: $table.requestDescription, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryCountry => $composableBuilder( + column: $table.deliveryCountry, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get offerProductName => $composableBuilder( + column: $table.offerProductName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get offerPrice => $composableBuilder( + column: $table.offerPrice, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingName => $composableBuilder( + column: $table.shippingName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingStreet => $composableBuilder( + column: $table.shippingStreet, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingCity => $composableBuilder( + column: $table.shippingCity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingPostalCode => $composableBuilder( + column: $table.shippingPostalCode, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingCountry => $composableBuilder( + column: $table.shippingCountry, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get messages => $composableBuilder( + column: $table.messages, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get apiTicketId => $composableBuilder( + column: $table.apiTicketId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get carResearchInvoiceId => $composableBuilder( + column: $table.carResearchInvoiceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get feeTicketNumber => $composableBuilder( + column: $table.feeTicketNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get needsCreateRequest => $composableBuilder( + column: $table.needsCreateRequest, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isPendingPayment => $composableBuilder( + column: $table.isPendingPayment, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get carResearchExpiresAt => $composableBuilder( + column: $table.carResearchExpiresAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get carResearchPaymentLinks => $composableBuilder( + column: $table.carResearchPaymentLinks, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ShopInBitTicketsTableAnnotationComposer + extends Composer<_$SharedDatabase, $ShopInBitTicketsTable> { + $$ShopInBitTicketsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get ticketId => + $composableBuilder(column: $table.ticketId, builder: (column) => column); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter get category => + $composableBuilder(column: $table.category, builder: (column) => column); + + GeneratedColumnWithTypeConverter get status => + $composableBuilder(column: $table.status, builder: (column) => column); + + GeneratedColumn get requestDescription => $composableBuilder( + column: $table.requestDescription, + builder: (column) => column, + ); + + GeneratedColumn get deliveryCountry => $composableBuilder( + column: $table.deliveryCountry, + builder: (column) => column, + ); + + GeneratedColumn get offerProductName => $composableBuilder( + column: $table.offerProductName, + builder: (column) => column, + ); + + GeneratedColumn get offerPrice => $composableBuilder( + column: $table.offerPrice, + builder: (column) => column, + ); + + GeneratedColumn get shippingName => $composableBuilder( + column: $table.shippingName, + builder: (column) => column, + ); + + GeneratedColumn get shippingStreet => $composableBuilder( + column: $table.shippingStreet, + builder: (column) => column, + ); + + GeneratedColumn get shippingCity => $composableBuilder( + column: $table.shippingCity, + builder: (column) => column, + ); + + GeneratedColumn get shippingPostalCode => $composableBuilder( + column: $table.shippingPostalCode, + builder: (column) => column, + ); + + GeneratedColumn get shippingCountry => $composableBuilder( + column: $table.shippingCountry, + builder: (column) => column, + ); + + GeneratedColumn get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter, String> + get messages => + $composableBuilder(column: $table.messages, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get apiTicketId => $composableBuilder( + column: $table.apiTicketId, + builder: (column) => column, + ); + + GeneratedColumn get carResearchInvoiceId => $composableBuilder( + column: $table.carResearchInvoiceId, + builder: (column) => column, + ); + + GeneratedColumn get feeTicketNumber => $composableBuilder( + column: $table.feeTicketNumber, + builder: (column) => column, + ); + + GeneratedColumn get needsCreateRequest => $composableBuilder( + column: $table.needsCreateRequest, + builder: (column) => column, + ); + + GeneratedColumn get isPendingPayment => $composableBuilder( + column: $table.isPendingPayment, + builder: (column) => column, + ); + + GeneratedColumn get carResearchExpiresAt => $composableBuilder( + column: $table.carResearchExpiresAt, + builder: (column) => column, + ); + + GeneratedColumn get carResearchPaymentLinks => $composableBuilder( + column: $table.carResearchPaymentLinks, + builder: (column) => column, + ); +} + +class $$ShopInBitTicketsTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $ShopInBitTicketsTable, + ShopInBitTicket, + $$ShopInBitTicketsTableFilterComposer, + $$ShopInBitTicketsTableOrderingComposer, + $$ShopInBitTicketsTableAnnotationComposer, + $$ShopInBitTicketsTableCreateCompanionBuilder, + $$ShopInBitTicketsTableUpdateCompanionBuilder, + ( + ShopInBitTicket, + BaseReferences< + _$SharedDatabase, + $ShopInBitTicketsTable, + ShopInBitTicket + >, + ), + ShopInBitTicket, + PrefetchHooks Function() + > { + $$ShopInBitTicketsTableTableManager( + _$SharedDatabase db, + $ShopInBitTicketsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShopInBitTicketsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShopInBitTicketsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShopInBitTicketsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value ticketId = const Value.absent(), + Value displayName = const Value.absent(), + Value category = const Value.absent(), + Value status = const Value.absent(), + Value requestDescription = const Value.absent(), + Value deliveryCountry = const Value.absent(), + Value offerProductName = const Value.absent(), + Value offerPrice = const Value.absent(), + Value shippingName = const Value.absent(), + Value shippingStreet = const Value.absent(), + Value shippingCity = const Value.absent(), + Value shippingPostalCode = const Value.absent(), + Value shippingCountry = const Value.absent(), + Value paymentMethod = const Value.absent(), + Value> messages = + const Value.absent(), + Value createdAt = const Value.absent(), + Value apiTicketId = const Value.absent(), + Value carResearchInvoiceId = const Value.absent(), + Value feeTicketNumber = const Value.absent(), + Value needsCreateRequest = const Value.absent(), + Value isPendingPayment = const Value.absent(), + Value carResearchExpiresAt = const Value.absent(), + Value carResearchPaymentLinks = const Value.absent(), + Value rowid = const Value.absent(), + }) => ShopInBitTicketsCompanion( + ticketId: ticketId, + displayName: displayName, + category: category, + status: status, + requestDescription: requestDescription, + deliveryCountry: deliveryCountry, + offerProductName: offerProductName, + offerPrice: offerPrice, + shippingName: shippingName, + shippingStreet: shippingStreet, + shippingCity: shippingCity, + shippingPostalCode: shippingPostalCode, + shippingCountry: shippingCountry, + paymentMethod: paymentMethod, + messages: messages, + createdAt: createdAt, + apiTicketId: apiTicketId, + carResearchInvoiceId: carResearchInvoiceId, + feeTicketNumber: feeTicketNumber, + needsCreateRequest: needsCreateRequest, + isPendingPayment: isPendingPayment, + carResearchExpiresAt: carResearchExpiresAt, + carResearchPaymentLinks: carResearchPaymentLinks, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String ticketId, + required String displayName, + required ShopInBitCategory category, + required ShopInBitOrderStatus status, + required String requestDescription, + required String deliveryCountry, + Value offerProductName = const Value.absent(), + Value offerPrice = const Value.absent(), + required String shippingName, + required String shippingStreet, + required String shippingCity, + required String shippingPostalCode, + required String shippingCountry, + Value paymentMethod = const Value.absent(), + required List messages, + required DateTime createdAt, + required int apiTicketId, + Value carResearchInvoiceId = const Value.absent(), + Value feeTicketNumber = const Value.absent(), + required bool needsCreateRequest, + required bool isPendingPayment, + Value carResearchExpiresAt = const Value.absent(), + Value carResearchPaymentLinks = const Value.absent(), + Value rowid = const Value.absent(), + }) => ShopInBitTicketsCompanion.insert( + ticketId: ticketId, + displayName: displayName, + category: category, + status: status, + requestDescription: requestDescription, + deliveryCountry: deliveryCountry, + offerProductName: offerProductName, + offerPrice: offerPrice, + shippingName: shippingName, + shippingStreet: shippingStreet, + shippingCity: shippingCity, + shippingPostalCode: shippingPostalCode, + shippingCountry: shippingCountry, + paymentMethod: paymentMethod, + messages: messages, + createdAt: createdAt, + apiTicketId: apiTicketId, + carResearchInvoiceId: carResearchInvoiceId, + feeTicketNumber: feeTicketNumber, + needsCreateRequest: needsCreateRequest, + isPendingPayment: isPendingPayment, + carResearchExpiresAt: carResearchExpiresAt, + carResearchPaymentLinks: carResearchPaymentLinks, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ShopInBitTicketsTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $ShopInBitTicketsTable, + ShopInBitTicket, + $$ShopInBitTicketsTableFilterComposer, + $$ShopInBitTicketsTableOrderingComposer, + $$ShopInBitTicketsTableAnnotationComposer, + $$ShopInBitTicketsTableCreateCompanionBuilder, + $$ShopInBitTicketsTableUpdateCompanionBuilder, + ( + ShopInBitTicket, + BaseReferences< + _$SharedDatabase, + $ShopInBitTicketsTable, + ShopInBitTicket + >, + ), + ShopInBitTicket, + PrefetchHooks Function() + >; + +class $SharedDatabaseManager { + final _$SharedDatabase _db; + $SharedDatabaseManager(this._db); + $$CakepayOrdersTableTableManager get cakepayOrders => + $$CakepayOrdersTableTableManager(_db, _db.cakepayOrders); + $$ShopinBitSettingsTableTableManager get shopinBitSettings => + $$ShopinBitSettingsTableTableManager(_db, _db.shopinBitSettings); + $$ShopInBitTicketsTableTableManager get shopInBitTickets => + $$ShopInBitTicketsTableTableManager(_db, _db.shopInBitTickets); +} + +mixin _$ShopinBitSettingsDaoMixin on DatabaseAccessor { + $ShopinBitSettingsTable get shopinBitSettings => + attachedDatabase.shopinBitSettings; + ShopinBitSettingsDaoManager get managers => ShopinBitSettingsDaoManager(this); +} + +class ShopinBitSettingsDaoManager { + final _$ShopinBitSettingsDaoMixin _db; + ShopinBitSettingsDaoManager(this._db); + $$ShopinBitSettingsTableTableManager get shopinBitSettings => + $$ShopinBitSettingsTableTableManager( + _db.attachedDatabase, + _db.shopinBitSettings, + ); +} diff --git a/lib/db/drift/shared_db/tables/cakepay_orders.dart b/lib/db/drift/shared_db/tables/cakepay_orders.dart new file mode 100644 index 000000000..8dc7f82e6 --- /dev/null +++ b/lib/db/drift/shared_db/tables/cakepay_orders.dart @@ -0,0 +1,8 @@ +import 'package:drift/drift.dart'; + +class CakepayOrders extends Table { + TextColumn get orderId => text()(); + + @override + Set get primaryKey => {orderId}; +} diff --git a/lib/db/drift/shared_db/tables/shopin_bit_settings.dart b/lib/db/drift/shared_db/tables/shopin_bit_settings.dart new file mode 100644 index 000000000..e4c32532e --- /dev/null +++ b/lib/db/drift/shared_db/tables/shopin_bit_settings.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; + +class ShopinBitSettings extends Table { + // Single row table - always row 0 + IntColumn get id => integer().withDefault(const Constant(0))(); + + BoolColumn get guidelinesAccepted => + boolean().withDefault(const Constant(false))(); + BoolColumn get setupComplete => + boolean().withDefault(const Constant(false))(); + TextColumn get displayName => text().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart b/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart new file mode 100644 index 000000000..b8afcc969 --- /dev/null +++ b/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart @@ -0,0 +1,112 @@ +import "dart:convert"; + +import "package:drift/drift.dart"; + +import '../../../../models/shopinbit/shopinbit_order_model.dart' + show ShopInBitCategory, ShopInBitOrderStatus; + +class ShopInBitTickets extends Table { + TextColumn get ticketId => text()(); + + TextColumn get displayName => text()(); + + IntColumn get category => intEnum()(); + IntColumn get status => intEnum()(); + + TextColumn get requestDescription => text()(); + TextColumn get deliveryCountry => text()(); + TextColumn get offerProductName => text().nullable()(); + TextColumn get offerPrice => text().nullable()(); + + TextColumn get shippingName => text()(); + TextColumn get shippingStreet => text()(); + TextColumn get shippingCity => text()(); + TextColumn get shippingPostalCode => text()(); + TextColumn get shippingCountry => text()(); + + TextColumn get paymentMethod => text().nullable()(); + + TextColumn get messages => + text().map(const ShopInBitTicketMessagesConverter())(); + + DateTimeColumn get createdAt => dateTime()(); + IntColumn get apiTicketId => integer()(); + + // Car research retry support + TextColumn get carResearchInvoiceId => text().nullable()(); + TextColumn get feeTicketNumber => text().nullable()(); + BoolColumn get needsCreateRequest => boolean()(); + + // Car research resumable payment state + BoolColumn get isPendingPayment => boolean()(); + DateTimeColumn get carResearchExpiresAt => dateTime().nullable()(); + TextColumn get carResearchPaymentLinks => text().nullable()(); + + @override + Set> get primaryKey => {ticketId}; +} + +class ShopInBitTicketMessage { + final String text; + final DateTime timestamp; + final bool isFromUser; + + const ShopInBitTicketMessage({ + required this.text, + required this.timestamp, + required this.isFromUser, + }); + + factory ShopInBitTicketMessage.fromJson(Map json) { + return ShopInBitTicketMessage( + text: json["text"] as String, + timestamp: DateTime.parse(json["timestamp"] as String), + isFromUser: json["isFromUser"] as bool, + ); + } + + Map toMap() { + return { + "text": text, + "timestamp": timestamp.toIso8601String(), + "isFromUser": isFromUser, + }; + } + + @override + String toString() => toMap().toString(); +} + +class ShopInBitTicketMessagesConverter + extends TypeConverter, String> + with + JsonTypeConverter2< + List, + String, + List + > { + const ShopInBitTicketMessagesConverter(); + + @override + List fromSql(String fromDb) { + final List decoded = jsonDecode(fromDb) as List; + return fromJson(decoded); + } + + @override + String toSql(List value) { + return jsonEncode(toJson(value)); + } + + @override + List fromJson(List json) { + return json + .map((e) => ShopInBitTicketMessage.fromJson(e as Map)) + .toList(); + } + + @override + List toJson(List value) { + return value.map((m) => m.toMap()).toList(); + } +} diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 6c6a3e8ba..3b86d7472 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -73,7 +73,6 @@ class MainDB { TokenWalletInfoSchema, FrostWalletInfoSchema, WalletSolanaTokenInfoSchema, - ShopInBitTicketSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, @@ -82,13 +81,6 @@ class MainDB { maxSizeMiB: Platform.isWindows ? 1024 : 512, ); - // Clear on schema mismatch; tickets are recoverable from the API. - try { - isar.shopInBitTickets.where().findAllSync(); - } catch (_) { - await isar.writeTxn(() async => isar.shopInBitTickets.clear()); - } - return true; } @@ -654,34 +646,4 @@ class MainDB { isar.writeTxn(() async { await isar.solContracts.putAll(tokens); }); - - // ========== ShopInBit tickets =============================================== - - List getShopInBitTickets() { - try { - return isar.shopInBitTickets.where().sortByCreatedAtDesc().findAllSync(); - } catch (_) { - return []; - } - } - - Future putShopInBitTicket(ShopInBitTicket ticket) async { - try { - return await isar.writeTxn(() async { - return await isar.shopInBitTickets.put(ticket); - }); - } catch (e) { - throw MainDBException("failed putShopInBitTicket", e); - } - } - - Future deleteShopInBitTicket(String ticketId) async { - try { - return await isar.writeTxn(() async { - return await isar.shopInBitTickets.deleteByTicketId(ticketId); - }); - } catch (e) { - throw MainDBException("failed deleteShopInBitTicket: $ticketId", e); - } - } } diff --git a/lib/models/isar/models/isar_models.dart b/lib/models/isar/models/isar_models.dart index 8206fc0f3..cf27091bf 100644 --- a/lib/models/isar/models/isar_models.dart +++ b/lib/models/isar/models/isar_models.dart @@ -17,6 +17,5 @@ export 'blockchain_data/utxo.dart'; export 'ethereum/eth_contract.dart'; export 'log.dart'; export 'solana/sol_contract.dart'; -export 'shopinbit_ticket.dart'; export 'transaction_note.dart'; export '../../../wallets/isar/models/wallet_solana_token_info.dart'; diff --git a/lib/models/isar/models/shopinbit_ticket.dart b/lib/models/isar/models/shopinbit_ticket.dart deleted file mode 100644 index 0a2ac53d7..000000000 --- a/lib/models/isar/models/shopinbit_ticket.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:isar_community/isar.dart'; - -import '../../shopinbit/shopinbit_order_model.dart'; - -part 'shopinbit_ticket.g.dart'; - -@collection -class ShopInBitTicket { - Id id = Isar.autoIncrement; - - @Index(unique: true, replace: true) - late String ticketId; - - late String displayName; - @enumerated - late ShopInBitCategory category; - @enumerated - late ShopInBitOrderStatus status; - late String requestDescription; - late String deliveryCountry; - late String? offerProductName; - late String? offerPrice; - late String shippingName; - late String shippingStreet; - late String shippingCity; - late String shippingPostalCode; - late String shippingCountry; - late String? paymentMethod; - late List messages; - late DateTime createdAt; - late int apiTicketId; - - // Car research retry support - String? carResearchInvoiceId; - String? feeTicketNumber; - late bool needsCreateRequest; - - // Car research resumable payment state - late bool isPendingPayment; - DateTime? carResearchExpiresAt; - String? carResearchPaymentLinks; -} - -@embedded -class ShopInBitTicketMessage { - late String text; - late DateTime timestamp; - late bool isFromUser; - - ShopInBitTicketMessage(); -} diff --git a/lib/models/isar/models/shopinbit_ticket.g.dart b/lib/models/isar/models/shopinbit_ticket.g.dart deleted file mode 100644 index ecd600a15..000000000 --- a/lib/models/isar/models/shopinbit_ticket.g.dart +++ /dev/null @@ -1,4651 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'shopinbit_ticket.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetShopInBitTicketCollection on Isar { - IsarCollection get shopInBitTickets => this.collection(); -} - -const ShopInBitTicketSchema = CollectionSchema( - name: r'ShopInBitTicket', - id: 1968691807160517649, - properties: { - r'apiTicketId': PropertySchema( - id: 0, - name: r'apiTicketId', - type: IsarType.long, - ), - r'carResearchExpiresAt': PropertySchema( - id: 1, - name: r'carResearchExpiresAt', - type: IsarType.dateTime, - ), - r'carResearchInvoiceId': PropertySchema( - id: 2, - name: r'carResearchInvoiceId', - type: IsarType.string, - ), - r'carResearchPaymentLinks': PropertySchema( - id: 3, - name: r'carResearchPaymentLinks', - type: IsarType.string, - ), - r'category': PropertySchema( - id: 4, - name: r'category', - type: IsarType.byte, - enumMap: _ShopInBitTicketcategoryEnumValueMap, - ), - r'createdAt': PropertySchema( - id: 5, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'deliveryCountry': PropertySchema( - id: 6, - name: r'deliveryCountry', - type: IsarType.string, - ), - r'displayName': PropertySchema( - id: 7, - name: r'displayName', - type: IsarType.string, - ), - r'feeTicketNumber': PropertySchema( - id: 8, - name: r'feeTicketNumber', - type: IsarType.string, - ), - r'isPendingPayment': PropertySchema( - id: 9, - name: r'isPendingPayment', - type: IsarType.bool, - ), - r'messages': PropertySchema( - id: 10, - name: r'messages', - type: IsarType.objectList, - - target: r'ShopInBitTicketMessage', - ), - r'needsCreateRequest': PropertySchema( - id: 11, - name: r'needsCreateRequest', - type: IsarType.bool, - ), - r'offerPrice': PropertySchema( - id: 12, - name: r'offerPrice', - type: IsarType.string, - ), - r'offerProductName': PropertySchema( - id: 13, - name: r'offerProductName', - type: IsarType.string, - ), - r'paymentMethod': PropertySchema( - id: 14, - name: r'paymentMethod', - type: IsarType.string, - ), - r'requestDescription': PropertySchema( - id: 15, - name: r'requestDescription', - type: IsarType.string, - ), - r'shippingCity': PropertySchema( - id: 16, - name: r'shippingCity', - type: IsarType.string, - ), - r'shippingCountry': PropertySchema( - id: 17, - name: r'shippingCountry', - type: IsarType.string, - ), - r'shippingName': PropertySchema( - id: 18, - name: r'shippingName', - type: IsarType.string, - ), - r'shippingPostalCode': PropertySchema( - id: 19, - name: r'shippingPostalCode', - type: IsarType.string, - ), - r'shippingStreet': PropertySchema( - id: 20, - name: r'shippingStreet', - type: IsarType.string, - ), - r'status': PropertySchema( - id: 21, - name: r'status', - type: IsarType.byte, - enumMap: _ShopInBitTicketstatusEnumValueMap, - ), - r'ticketId': PropertySchema( - id: 22, - name: r'ticketId', - type: IsarType.string, - ), - }, - - estimateSize: _shopInBitTicketEstimateSize, - serialize: _shopInBitTicketSerialize, - deserialize: _shopInBitTicketDeserialize, - deserializeProp: _shopInBitTicketDeserializeProp, - idName: r'id', - indexes: { - r'ticketId': IndexSchema( - id: -6483959237056329942, - name: r'ticketId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'ticketId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {r'ShopInBitTicketMessage': ShopInBitTicketMessageSchema}, - - getId: _shopInBitTicketGetId, - getLinks: _shopInBitTicketGetLinks, - attach: _shopInBitTicketAttach, - version: '3.3.0-dev.2', -); - -int _shopInBitTicketEstimateSize( - ShopInBitTicket object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.carResearchInvoiceId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.carResearchPaymentLinks; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.deliveryCountry.length * 3; - bytesCount += 3 + object.displayName.length * 3; - { - final value = object.feeTicketNumber; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.messages.length * 3; - { - final offsets = allOffsets[ShopInBitTicketMessage]!; - for (var i = 0; i < object.messages.length; i++) { - final value = object.messages[i]; - bytesCount += ShopInBitTicketMessageSchema.estimateSize( - value, - offsets, - allOffsets, - ); - } - } - { - final value = object.offerPrice; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.offerProductName; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.paymentMethod; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.requestDescription.length * 3; - bytesCount += 3 + object.shippingCity.length * 3; - bytesCount += 3 + object.shippingCountry.length * 3; - bytesCount += 3 + object.shippingName.length * 3; - bytesCount += 3 + object.shippingPostalCode.length * 3; - bytesCount += 3 + object.shippingStreet.length * 3; - bytesCount += 3 + object.ticketId.length * 3; - return bytesCount; -} - -void _shopInBitTicketSerialize( - ShopInBitTicket object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.apiTicketId); - writer.writeDateTime(offsets[1], object.carResearchExpiresAt); - writer.writeString(offsets[2], object.carResearchInvoiceId); - writer.writeString(offsets[3], object.carResearchPaymentLinks); - writer.writeByte(offsets[4], object.category.index); - writer.writeDateTime(offsets[5], object.createdAt); - writer.writeString(offsets[6], object.deliveryCountry); - writer.writeString(offsets[7], object.displayName); - writer.writeString(offsets[8], object.feeTicketNumber); - writer.writeBool(offsets[9], object.isPendingPayment); - writer.writeObjectList( - offsets[10], - allOffsets, - ShopInBitTicketMessageSchema.serialize, - object.messages, - ); - writer.writeBool(offsets[11], object.needsCreateRequest); - writer.writeString(offsets[12], object.offerPrice); - writer.writeString(offsets[13], object.offerProductName); - writer.writeString(offsets[14], object.paymentMethod); - writer.writeString(offsets[15], object.requestDescription); - writer.writeString(offsets[16], object.shippingCity); - writer.writeString(offsets[17], object.shippingCountry); - writer.writeString(offsets[18], object.shippingName); - writer.writeString(offsets[19], object.shippingPostalCode); - writer.writeString(offsets[20], object.shippingStreet); - writer.writeByte(offsets[21], object.status.index); - writer.writeString(offsets[22], object.ticketId); -} - -ShopInBitTicket _shopInBitTicketDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ShopInBitTicket(); - object.apiTicketId = reader.readLong(offsets[0]); - object.carResearchExpiresAt = reader.readDateTimeOrNull(offsets[1]); - object.carResearchInvoiceId = reader.readStringOrNull(offsets[2]); - object.carResearchPaymentLinks = reader.readStringOrNull(offsets[3]); - object.category = - _ShopInBitTicketcategoryValueEnumMap[reader.readByteOrNull(offsets[4])] ?? - ShopInBitCategory.concierge; - object.createdAt = reader.readDateTime(offsets[5]); - object.deliveryCountry = reader.readString(offsets[6]); - object.displayName = reader.readString(offsets[7]); - object.feeTicketNumber = reader.readStringOrNull(offsets[8]); - object.id = id; - object.isPendingPayment = reader.readBool(offsets[9]); - object.messages = - reader.readObjectList( - offsets[10], - ShopInBitTicketMessageSchema.deserialize, - allOffsets, - ShopInBitTicketMessage(), - ) ?? - []; - object.needsCreateRequest = reader.readBool(offsets[11]); - object.offerPrice = reader.readStringOrNull(offsets[12]); - object.offerProductName = reader.readStringOrNull(offsets[13]); - object.paymentMethod = reader.readStringOrNull(offsets[14]); - object.requestDescription = reader.readString(offsets[15]); - object.shippingCity = reader.readString(offsets[16]); - object.shippingCountry = reader.readString(offsets[17]); - object.shippingName = reader.readString(offsets[18]); - object.shippingPostalCode = reader.readString(offsets[19]); - object.shippingStreet = reader.readString(offsets[20]); - object.status = - _ShopInBitTicketstatusValueEnumMap[reader.readByteOrNull(offsets[21])] ?? - ShopInBitOrderStatus.pending; - object.ticketId = reader.readString(offsets[22]); - return object; -} - -P _shopInBitTicketDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLong(offset)) as P; - case 1: - return (reader.readDateTimeOrNull(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (_ShopInBitTicketcategoryValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - ShopInBitCategory.concierge) - as P; - case 5: - return (reader.readDateTime(offset)) as P; - case 6: - return (reader.readString(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (reader.readObjectList( - offset, - ShopInBitTicketMessageSchema.deserialize, - allOffsets, - ShopInBitTicketMessage(), - ) ?? - []) - as P; - case 11: - return (reader.readBool(offset)) as P; - case 12: - return (reader.readStringOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readString(offset)) as P; - case 16: - return (reader.readString(offset)) as P; - case 17: - return (reader.readString(offset)) as P; - case 18: - return (reader.readString(offset)) as P; - case 19: - return (reader.readString(offset)) as P; - case 20: - return (reader.readString(offset)) as P; - case 21: - return (_ShopInBitTicketstatusValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - ShopInBitOrderStatus.pending) - as P; - case 22: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _ShopInBitTicketcategoryEnumValueMap = { - 'concierge': 0, - 'travel': 1, - 'car': 2, -}; -const _ShopInBitTicketcategoryValueEnumMap = { - 0: ShopInBitCategory.concierge, - 1: ShopInBitCategory.travel, - 2: ShopInBitCategory.car, -}; -const _ShopInBitTicketstatusEnumValueMap = { - 'pending': 0, - 'reviewing': 1, - 'offerAvailable': 2, - 'accepted': 3, - 'paymentPending': 4, - 'paid': 5, - 'shipping': 6, - 'delivered': 7, - 'closed': 8, - 'cancelled': 9, - 'refunded': 10, -}; -const _ShopInBitTicketstatusValueEnumMap = { - 0: ShopInBitOrderStatus.pending, - 1: ShopInBitOrderStatus.reviewing, - 2: ShopInBitOrderStatus.offerAvailable, - 3: ShopInBitOrderStatus.accepted, - 4: ShopInBitOrderStatus.paymentPending, - 5: ShopInBitOrderStatus.paid, - 6: ShopInBitOrderStatus.shipping, - 7: ShopInBitOrderStatus.delivered, - 8: ShopInBitOrderStatus.closed, - 9: ShopInBitOrderStatus.cancelled, - 10: ShopInBitOrderStatus.refunded, -}; - -Id _shopInBitTicketGetId(ShopInBitTicket object) { - return object.id; -} - -List> _shopInBitTicketGetLinks(ShopInBitTicket object) { - return []; -} - -void _shopInBitTicketAttach( - IsarCollection col, - Id id, - ShopInBitTicket object, -) { - object.id = id; -} - -extension ShopInBitTicketByIndex on IsarCollection { - Future getByTicketId(String ticketId) { - return getByIndex(r'ticketId', [ticketId]); - } - - ShopInBitTicket? getByTicketIdSync(String ticketId) { - return getByIndexSync(r'ticketId', [ticketId]); - } - - Future deleteByTicketId(String ticketId) { - return deleteByIndex(r'ticketId', [ticketId]); - } - - bool deleteByTicketIdSync(String ticketId) { - return deleteByIndexSync(r'ticketId', [ticketId]); - } - - Future> getAllByTicketId(List ticketIdValues) { - final values = ticketIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'ticketId', values); - } - - List getAllByTicketIdSync(List ticketIdValues) { - final values = ticketIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'ticketId', values); - } - - Future deleteAllByTicketId(List ticketIdValues) { - final values = ticketIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'ticketId', values); - } - - int deleteAllByTicketIdSync(List ticketIdValues) { - final values = ticketIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'ticketId', values); - } - - Future putByTicketId(ShopInBitTicket object) { - return putByIndex(r'ticketId', object); - } - - Id putByTicketIdSync(ShopInBitTicket object, {bool saveLinks = true}) { - return putByIndexSync(r'ticketId', object, saveLinks: saveLinks); - } - - Future> putAllByTicketId(List objects) { - return putAllByIndex(r'ticketId', objects); - } - - List putAllByTicketIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'ticketId', objects, saveLinks: saveLinks); - } -} - -extension ShopInBitTicketQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ShopInBitTicketQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo( - Id id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - ticketIdEqualTo(String ticketId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'ticketId', value: [ticketId]), - ); - }); - } - - QueryBuilder - ticketIdNotEqualTo(String ticketId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ticketId', - lower: [], - upper: [ticketId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ticketId', - lower: [ticketId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ticketId', - lower: [ticketId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ticketId', - lower: [], - upper: [ticketId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ShopInBitTicketQueryFilter - on QueryBuilder { - QueryBuilder - apiTicketIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'apiTicketId', value: value), - ); - }); - } - - QueryBuilder - apiTicketIdGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'apiTicketId', - value: value, - ), - ); - }); - } - - QueryBuilder - apiTicketIdLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'apiTicketId', - value: value, - ), - ); - }); - } - - QueryBuilder - apiTicketIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'apiTicketId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - carResearchExpiresAtIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'carResearchExpiresAt'), - ); - }); - } - - QueryBuilder - carResearchExpiresAtIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'carResearchExpiresAt'), - ); - }); - } - - QueryBuilder - carResearchExpiresAtEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'carResearchExpiresAt', - value: value, - ), - ); - }); - } - - QueryBuilder - carResearchExpiresAtGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'carResearchExpiresAt', - value: value, - ), - ); - }); - } - - QueryBuilder - carResearchExpiresAtLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'carResearchExpiresAt', - value: value, - ), - ); - }); - } - - QueryBuilder - carResearchExpiresAtBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'carResearchExpiresAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'carResearchInvoiceId'), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'carResearchInvoiceId'), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'carResearchInvoiceId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'carResearchInvoiceId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'carResearchInvoiceId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'carResearchInvoiceId', value: ''), - ); - }); - } - - QueryBuilder - carResearchInvoiceIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'carResearchInvoiceId', - value: '', - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'carResearchPaymentLinks'), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'carResearchPaymentLinks'), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'carResearchPaymentLinks', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'carResearchPaymentLinks', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'carResearchPaymentLinks', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'carResearchPaymentLinks', - value: '', - ), - ); - }); - } - - QueryBuilder - carResearchPaymentLinksIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'carResearchPaymentLinks', - value: '', - ), - ); - }); - } - - QueryBuilder - categoryEqualTo(ShopInBitCategory value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'category', value: value), - ); - }); - } - - QueryBuilder - categoryGreaterThan(ShopInBitCategory value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'category', - value: value, - ), - ); - }); - } - - QueryBuilder - categoryLessThan(ShopInBitCategory value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'category', - value: value, - ), - ); - }); - } - - QueryBuilder - categoryBetween( - ShopInBitCategory lower, - ShopInBitCategory upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'category', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - createdAtEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder - createdAtGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder - createdAtLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder - createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - deliveryCountryEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'deliveryCountry', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'deliveryCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'deliveryCountry', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - deliveryCountryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'deliveryCountry', value: ''), - ); - }); - } - - QueryBuilder - deliveryCountryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'deliveryCountry', value: ''), - ); - }); - } - - QueryBuilder - displayNameEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'displayName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'displayName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'displayName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - displayNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'displayName', value: ''), - ); - }); - } - - QueryBuilder - displayNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'displayName', value: ''), - ); - }); - } - - QueryBuilder - feeTicketNumberIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'feeTicketNumber'), - ); - }); - } - - QueryBuilder - feeTicketNumberIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'feeTicketNumber'), - ); - }); - } - - QueryBuilder - feeTicketNumberEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'feeTicketNumber', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'feeTicketNumber', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'feeTicketNumber', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - feeTicketNumberIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'feeTicketNumber', value: ''), - ); - }); - } - - QueryBuilder - feeTicketNumberIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'feeTicketNumber', value: ''), - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - isPendingPaymentEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPendingPayment', value: value), - ); - }); - } - - QueryBuilder - messagesLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'messages', length, true, length, true); - }); - } - - QueryBuilder - messagesIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'messages', 0, true, 0, true); - }); - } - - QueryBuilder - messagesIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'messages', 0, false, 999999, true); - }); - } - - QueryBuilder - messagesLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'messages', 0, true, length, include); - }); - } - - QueryBuilder - messagesLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'messages', length, include, 999999, true); - }); - } - - QueryBuilder - messagesLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'messages', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - needsCreateRequestEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'needsCreateRequest', value: value), - ); - }); - } - - QueryBuilder - offerPriceIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'offerPrice'), - ); - }); - } - - QueryBuilder - offerPriceIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'offerPrice'), - ); - }); - } - - QueryBuilder - offerPriceEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'offerPrice', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'offerPrice', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'offerPrice', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerPriceIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'offerPrice', value: ''), - ); - }); - } - - QueryBuilder - offerPriceIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'offerPrice', value: ''), - ); - }); - } - - QueryBuilder - offerProductNameIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'offerProductName'), - ); - }); - } - - QueryBuilder - offerProductNameIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'offerProductName'), - ); - }); - } - - QueryBuilder - offerProductNameEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'offerProductName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'offerProductName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'offerProductName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - offerProductNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'offerProductName', value: ''), - ); - }); - } - - QueryBuilder - offerProductNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'offerProductName', value: ''), - ); - }); - } - - QueryBuilder - paymentMethodIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'paymentMethod'), - ); - }); - } - - QueryBuilder - paymentMethodIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'paymentMethod'), - ); - }); - } - - QueryBuilder - paymentMethodEqualTo(String? value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'paymentMethod', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'paymentMethod', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'paymentMethod', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - paymentMethodIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'paymentMethod', value: ''), - ); - }); - } - - QueryBuilder - paymentMethodIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'paymentMethod', value: ''), - ); - }); - } - - QueryBuilder - requestDescriptionEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'requestDescription', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'requestDescription', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'requestDescription', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - requestDescriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'requestDescription', value: ''), - ); - }); - } - - QueryBuilder - requestDescriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'requestDescription', value: ''), - ); - }); - } - - QueryBuilder - shippingCityEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'shippingCity', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'shippingCity', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'shippingCity', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shippingCity', value: ''), - ); - }); - } - - QueryBuilder - shippingCityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'shippingCity', value: ''), - ); - }); - } - - QueryBuilder - shippingCountryEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'shippingCountry', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'shippingCountry', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'shippingCountry', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingCountryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shippingCountry', value: ''), - ); - }); - } - - QueryBuilder - shippingCountryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'shippingCountry', value: ''), - ); - }); - } - - QueryBuilder - shippingNameEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'shippingName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'shippingName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'shippingName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shippingName', value: ''), - ); - }); - } - - QueryBuilder - shippingNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'shippingName', value: ''), - ); - }); - } - - QueryBuilder - shippingPostalCodeEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'shippingPostalCode', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'shippingPostalCode', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'shippingPostalCode', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingPostalCodeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shippingPostalCode', value: ''), - ); - }); - } - - QueryBuilder - shippingPostalCodeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'shippingPostalCode', value: ''), - ); - }); - } - - QueryBuilder - shippingStreetEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'shippingStreet', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'shippingStreet', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'shippingStreet', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - shippingStreetIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shippingStreet', value: ''), - ); - }); - } - - QueryBuilder - shippingStreetIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'shippingStreet', value: ''), - ); - }); - } - - QueryBuilder - statusEqualTo(ShopInBitOrderStatus value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'status', value: value), - ); - }); - } - - QueryBuilder - statusGreaterThan(ShopInBitOrderStatus value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'status', - value: value, - ), - ); - }); - } - - QueryBuilder - statusLessThan(ShopInBitOrderStatus value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'status', - value: value, - ), - ); - }); - } - - QueryBuilder - statusBetween( - ShopInBitOrderStatus lower, - ShopInBitOrderStatus upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'status', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - ticketIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ticketId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'ticketId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'ticketId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - ticketIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ticketId', value: ''), - ); - }); - } - - QueryBuilder - ticketIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'ticketId', value: ''), - ); - }); - } -} - -extension ShopInBitTicketQueryObject - on QueryBuilder { - QueryBuilder - messagesElement(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.object(q, r'messages'); - }); - } -} - -extension ShopInBitTicketQueryLinks - on QueryBuilder {} - -extension ShopInBitTicketQuerySortBy - on QueryBuilder { - QueryBuilder - sortByApiTicketId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'apiTicketId', Sort.asc); - }); - } - - QueryBuilder - sortByApiTicketIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'apiTicketId', Sort.desc); - }); - } - - QueryBuilder - sortByCarResearchExpiresAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchExpiresAt', Sort.asc); - }); - } - - QueryBuilder - sortByCarResearchExpiresAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchExpiresAt', Sort.desc); - }); - } - - QueryBuilder - sortByCarResearchInvoiceId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchInvoiceId', Sort.asc); - }); - } - - QueryBuilder - sortByCarResearchInvoiceIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchInvoiceId', Sort.desc); - }); - } - - QueryBuilder - sortByCarResearchPaymentLinks() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchPaymentLinks', Sort.asc); - }); - } - - QueryBuilder - sortByCarResearchPaymentLinksDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchPaymentLinks', Sort.desc); - }); - } - - QueryBuilder - sortByCategory() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'category', Sort.asc); - }); - } - - QueryBuilder - sortByCategoryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'category', Sort.desc); - }); - } - - QueryBuilder - sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder - sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder - sortByDeliveryCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'deliveryCountry', Sort.asc); - }); - } - - QueryBuilder - sortByDeliveryCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'deliveryCountry', Sort.desc); - }); - } - - QueryBuilder - sortByDisplayName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'displayName', Sort.asc); - }); - } - - QueryBuilder - sortByDisplayNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'displayName', Sort.desc); - }); - } - - QueryBuilder - sortByFeeTicketNumber() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'feeTicketNumber', Sort.asc); - }); - } - - QueryBuilder - sortByFeeTicketNumberDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'feeTicketNumber', Sort.desc); - }); - } - - QueryBuilder - sortByIsPendingPayment() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPendingPayment', Sort.asc); - }); - } - - QueryBuilder - sortByIsPendingPaymentDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPendingPayment', Sort.desc); - }); - } - - QueryBuilder - sortByNeedsCreateRequest() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'needsCreateRequest', Sort.asc); - }); - } - - QueryBuilder - sortByNeedsCreateRequestDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'needsCreateRequest', Sort.desc); - }); - } - - QueryBuilder - sortByOfferPrice() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerPrice', Sort.asc); - }); - } - - QueryBuilder - sortByOfferPriceDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerPrice', Sort.desc); - }); - } - - QueryBuilder - sortByOfferProductName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerProductName', Sort.asc); - }); - } - - QueryBuilder - sortByOfferProductNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerProductName', Sort.desc); - }); - } - - QueryBuilder - sortByPaymentMethod() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'paymentMethod', Sort.asc); - }); - } - - QueryBuilder - sortByPaymentMethodDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'paymentMethod', Sort.desc); - }); - } - - QueryBuilder - sortByRequestDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'requestDescription', Sort.asc); - }); - } - - QueryBuilder - sortByRequestDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'requestDescription', Sort.desc); - }); - } - - QueryBuilder - sortByShippingCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCity', Sort.asc); - }); - } - - QueryBuilder - sortByShippingCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCity', Sort.desc); - }); - } - - QueryBuilder - sortByShippingCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCountry', Sort.asc); - }); - } - - QueryBuilder - sortByShippingCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCountry', Sort.desc); - }); - } - - QueryBuilder - sortByShippingName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingName', Sort.asc); - }); - } - - QueryBuilder - sortByShippingNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingName', Sort.desc); - }); - } - - QueryBuilder - sortByShippingPostalCode() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingPostalCode', Sort.asc); - }); - } - - QueryBuilder - sortByShippingPostalCodeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingPostalCode', Sort.desc); - }); - } - - QueryBuilder - sortByShippingStreet() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingStreet', Sort.asc); - }); - } - - QueryBuilder - sortByShippingStreetDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingStreet', Sort.desc); - }); - } - - QueryBuilder sortByStatus() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'status', Sort.asc); - }); - } - - QueryBuilder - sortByStatusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'status', Sort.desc); - }); - } - - QueryBuilder - sortByTicketId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ticketId', Sort.asc); - }); - } - - QueryBuilder - sortByTicketIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ticketId', Sort.desc); - }); - } -} - -extension ShopInBitTicketQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByApiTicketId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'apiTicketId', Sort.asc); - }); - } - - QueryBuilder - thenByApiTicketIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'apiTicketId', Sort.desc); - }); - } - - QueryBuilder - thenByCarResearchExpiresAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchExpiresAt', Sort.asc); - }); - } - - QueryBuilder - thenByCarResearchExpiresAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchExpiresAt', Sort.desc); - }); - } - - QueryBuilder - thenByCarResearchInvoiceId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchInvoiceId', Sort.asc); - }); - } - - QueryBuilder - thenByCarResearchInvoiceIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchInvoiceId', Sort.desc); - }); - } - - QueryBuilder - thenByCarResearchPaymentLinks() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchPaymentLinks', Sort.asc); - }); - } - - QueryBuilder - thenByCarResearchPaymentLinksDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'carResearchPaymentLinks', Sort.desc); - }); - } - - QueryBuilder - thenByCategory() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'category', Sort.asc); - }); - } - - QueryBuilder - thenByCategoryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'category', Sort.desc); - }); - } - - QueryBuilder - thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder - thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder - thenByDeliveryCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'deliveryCountry', Sort.asc); - }); - } - - QueryBuilder - thenByDeliveryCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'deliveryCountry', Sort.desc); - }); - } - - QueryBuilder - thenByDisplayName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'displayName', Sort.asc); - }); - } - - QueryBuilder - thenByDisplayNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'displayName', Sort.desc); - }); - } - - QueryBuilder - thenByFeeTicketNumber() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'feeTicketNumber', Sort.asc); - }); - } - - QueryBuilder - thenByFeeTicketNumberDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'feeTicketNumber', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByIsPendingPayment() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPendingPayment', Sort.asc); - }); - } - - QueryBuilder - thenByIsPendingPaymentDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPendingPayment', Sort.desc); - }); - } - - QueryBuilder - thenByNeedsCreateRequest() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'needsCreateRequest', Sort.asc); - }); - } - - QueryBuilder - thenByNeedsCreateRequestDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'needsCreateRequest', Sort.desc); - }); - } - - QueryBuilder - thenByOfferPrice() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerPrice', Sort.asc); - }); - } - - QueryBuilder - thenByOfferPriceDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerPrice', Sort.desc); - }); - } - - QueryBuilder - thenByOfferProductName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerProductName', Sort.asc); - }); - } - - QueryBuilder - thenByOfferProductNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'offerProductName', Sort.desc); - }); - } - - QueryBuilder - thenByPaymentMethod() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'paymentMethod', Sort.asc); - }); - } - - QueryBuilder - thenByPaymentMethodDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'paymentMethod', Sort.desc); - }); - } - - QueryBuilder - thenByRequestDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'requestDescription', Sort.asc); - }); - } - - QueryBuilder - thenByRequestDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'requestDescription', Sort.desc); - }); - } - - QueryBuilder - thenByShippingCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCity', Sort.asc); - }); - } - - QueryBuilder - thenByShippingCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCity', Sort.desc); - }); - } - - QueryBuilder - thenByShippingCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCountry', Sort.asc); - }); - } - - QueryBuilder - thenByShippingCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingCountry', Sort.desc); - }); - } - - QueryBuilder - thenByShippingName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingName', Sort.asc); - }); - } - - QueryBuilder - thenByShippingNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingName', Sort.desc); - }); - } - - QueryBuilder - thenByShippingPostalCode() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingPostalCode', Sort.asc); - }); - } - - QueryBuilder - thenByShippingPostalCodeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingPostalCode', Sort.desc); - }); - } - - QueryBuilder - thenByShippingStreet() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingStreet', Sort.asc); - }); - } - - QueryBuilder - thenByShippingStreetDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shippingStreet', Sort.desc); - }); - } - - QueryBuilder thenByStatus() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'status', Sort.asc); - }); - } - - QueryBuilder - thenByStatusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'status', Sort.desc); - }); - } - - QueryBuilder - thenByTicketId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ticketId', Sort.asc); - }); - } - - QueryBuilder - thenByTicketIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ticketId', Sort.desc); - }); - } -} - -extension ShopInBitTicketQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByApiTicketId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'apiTicketId'); - }); - } - - QueryBuilder - distinctByCarResearchExpiresAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'carResearchExpiresAt'); - }); - } - - QueryBuilder - distinctByCarResearchInvoiceId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'carResearchInvoiceId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByCarResearchPaymentLinks({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'carResearchPaymentLinks', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByCategory() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'category'); - }); - } - - QueryBuilder - distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder - distinctByDeliveryCountry({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'deliveryCountry', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByDisplayName({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'displayName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByFeeTicketNumber({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'feeTicketNumber', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByIsPendingPayment() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPendingPayment'); - }); - } - - QueryBuilder - distinctByNeedsCreateRequest() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'needsCreateRequest'); - }); - } - - QueryBuilder - distinctByOfferPrice({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'offerPrice', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByOfferProductName({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'offerProductName', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByPaymentMethod({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'paymentMethod', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByRequestDescription({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'requestDescription', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByShippingCity({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shippingCity', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByShippingCountry({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'shippingCountry', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByShippingName({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shippingName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByShippingPostalCode({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'shippingPostalCode', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder - distinctByShippingStreet({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'shippingStreet', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByStatus() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'status'); - }); - } - - QueryBuilder distinctByTicketId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ticketId', caseSensitive: caseSensitive); - }); - } -} - -extension ShopInBitTicketQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder apiTicketIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'apiTicketId'); - }); - } - - QueryBuilder - carResearchExpiresAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'carResearchExpiresAt'); - }); - } - - QueryBuilder - carResearchInvoiceIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'carResearchInvoiceId'); - }); - } - - QueryBuilder - carResearchPaymentLinksProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'carResearchPaymentLinks'); - }); - } - - QueryBuilder - categoryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'category'); - }); - } - - QueryBuilder - createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder - deliveryCountryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'deliveryCountry'); - }); - } - - QueryBuilder - displayNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'displayName'); - }); - } - - QueryBuilder - feeTicketNumberProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'feeTicketNumber'); - }); - } - - QueryBuilder - isPendingPaymentProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPendingPayment'); - }); - } - - QueryBuilder, QQueryOperations> - messagesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'messages'); - }); - } - - QueryBuilder - needsCreateRequestProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'needsCreateRequest'); - }); - } - - QueryBuilder - offerPriceProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'offerPrice'); - }); - } - - QueryBuilder - offerProductNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'offerProductName'); - }); - } - - QueryBuilder - paymentMethodProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'paymentMethod'); - }); - } - - QueryBuilder - requestDescriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'requestDescription'); - }); - } - - QueryBuilder - shippingCityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shippingCity'); - }); - } - - QueryBuilder - shippingCountryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shippingCountry'); - }); - } - - QueryBuilder - shippingNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shippingName'); - }); - } - - QueryBuilder - shippingPostalCodeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shippingPostalCode'); - }); - } - - QueryBuilder - shippingStreetProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shippingStreet'); - }); - } - - QueryBuilder - statusProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'status'); - }); - } - - QueryBuilder ticketIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ticketId'); - }); - } -} - -// ************************************************************************** -// IsarEmbeddedGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -const ShopInBitTicketMessageSchema = Schema( - name: r'ShopInBitTicketMessage', - id: -6797752334657665095, - properties: { - r'isFromUser': PropertySchema( - id: 0, - name: r'isFromUser', - type: IsarType.bool, - ), - r'text': PropertySchema(id: 1, name: r'text', type: IsarType.string), - r'timestamp': PropertySchema( - id: 2, - name: r'timestamp', - type: IsarType.dateTime, - ), - }, - - estimateSize: _shopInBitTicketMessageEstimateSize, - serialize: _shopInBitTicketMessageSerialize, - deserialize: _shopInBitTicketMessageDeserialize, - deserializeProp: _shopInBitTicketMessageDeserializeProp, -); - -int _shopInBitTicketMessageEstimateSize( - ShopInBitTicketMessage object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.text.length * 3; - return bytesCount; -} - -void _shopInBitTicketMessageSerialize( - ShopInBitTicketMessage object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.isFromUser); - writer.writeString(offsets[1], object.text); - writer.writeDateTime(offsets[2], object.timestamp); -} - -ShopInBitTicketMessage _shopInBitTicketMessageDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ShopInBitTicketMessage(); - object.isFromUser = reader.readBool(offsets[0]); - object.text = reader.readString(offsets[1]); - object.timestamp = reader.readDateTime(offsets[2]); - return object; -} - -P _shopInBitTicketMessageDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -extension ShopInBitTicketMessageQueryFilter - on - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QFilterCondition - > { - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - isFromUserEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFromUser', value: value), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'text', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'text', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'text', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'text', value: ''), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - textIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'text', value: ''), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - timestampEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timestamp', value: value), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - timestampGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timestamp', - value: value, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - timestampLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timestamp', - value: value, - ), - ); - }); - } - - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QAfterFilterCondition - > - timestampBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ShopInBitTicketMessageQueryObject - on - QueryBuilder< - ShopInBitTicketMessage, - ShopInBitTicketMessage, - QFilterCondition - > {} diff --git a/lib/models/shopinbit/shopinbit_order_model.dart b/lib/models/shopinbit/shopinbit_order_model.dart index 88d247c79..c7d4e4df2 100644 --- a/lib/models/shopinbit/shopinbit_order_model.dart +++ b/lib/models/shopinbit/shopinbit_order_model.dart @@ -1,13 +1,17 @@ import 'dart:ui'; +import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; +import '../../db/drift/shared_db/shared_database.dart'; +import '../../db/drift/shared_db/tables/shopin_bit_tickets.dart'; import '../../services/shopinbit/src/models/ticket.dart'; import '../../themes/stack_colors.dart'; -import '../isar/models/shopinbit_ticket.dart'; +// these enum indexes are stored in a db. Do not edit order enum ShopInBitCategory { concierge, travel, car } +// these enum indexes are stored in a db. Do not edit order enum ShopInBitOrderStatus { pending, reviewing, @@ -255,41 +259,57 @@ class ShopInBitOrderModel extends ChangeNotifier { _messages.clear(); } - ShopInBitTicket toIsarTicket() { - return ShopInBitTicket() - ..ticketId = _ticketId ?? "" - ..displayName = _displayName - ..category = _category ?? ShopInBitCategory.concierge - ..status = _status - ..requestDescription = _requestDescription - ..deliveryCountry = _deliveryCountry - ..offerProductName = _offerProductName - ..offerPrice = _offerPrice - ..shippingName = _shippingName - ..shippingStreet = _shippingStreet - ..shippingCity = _shippingCity - ..shippingPostalCode = _shippingPostalCode - ..shippingCountry = _shippingCountry - ..paymentMethod = _paymentMethod - ..apiTicketId = _apiTicketId - ..carResearchInvoiceId = _carResearchInvoiceId - ..feeTicketNumber = _feeTicketNumber - ..needsCreateRequest = _needsCreateRequest - ..isPendingPayment = _isPendingPayment - ..carResearchExpiresAt = _carResearchExpiresAt - ..carResearchPaymentLinks = _carResearchPaymentLinks - ..messages = _messages - .map( - (m) => ShopInBitTicketMessage() - ..text = m.text - ..timestamp = m.timestamp - ..isFromUser = m.isFromUser, - ) - .toList() - ..createdAt = DateTime.now(); + ShopInBitTicketsCompanion toCompanion() { + assert(_ticketId != null, "ticketId must be set before persisting"); + + final List messages = _messages + .map( + (m) => ShopInBitTicketMessage( + text: m.text, + timestamp: m.timestamp, + isFromUser: m.isFromUser, + ), + ) + .toList(); + + return ShopInBitTicketsCompanion( + ticketId: Value(_ticketId!), + displayName: Value(_displayName), + category: Value(_category ?? ShopInBitCategory.concierge), + status: Value(_status), + requestDescription: Value(_requestDescription), + deliveryCountry: Value(_deliveryCountry), + offerProductName: Value(_offerProductName), + offerPrice: Value(_offerPrice), + shippingName: Value(_shippingName), + shippingStreet: Value(_shippingStreet), + shippingCity: Value(_shippingCity), + shippingPostalCode: Value(_shippingPostalCode), + shippingCountry: Value(_shippingCountry), + paymentMethod: Value(_paymentMethod), + apiTicketId: Value(_apiTicketId), + carResearchInvoiceId: Value(_carResearchInvoiceId), + feeTicketNumber: Value(_feeTicketNumber), + needsCreateRequest: Value(_needsCreateRequest), + isPendingPayment: Value(_isPendingPayment), + carResearchExpiresAt: Value(_carResearchExpiresAt), + carResearchPaymentLinks: Value(_carResearchPaymentLinks), + messages: Value(messages), + createdAt: Value(DateTime.now()), + ); } - static ShopInBitOrderModel fromIsarTicket(ShopInBitTicket ticket) { + static ShopInBitOrderModel fromDriftRow(ShopInBitTicket ticket) { + final List messages = ticket.messages + .map( + (m) => ShopInBitMessage( + text: m.text, + timestamp: m.timestamp, + isFromUser: m.isFromUser, + ), + ) + .toList(); + return ShopInBitOrderModel() .._displayName = ticket.displayName .._category = ticket.category @@ -312,15 +332,7 @@ class ShopInBitOrderModel extends ChangeNotifier { .._isPendingPayment = ticket.isPendingPayment .._carResearchExpiresAt = ticket.carResearchExpiresAt .._carResearchPaymentLinks = ticket.carResearchPaymentLinks - .._messages = ticket.messages - .map( - (m) => ShopInBitMessage( - text: m.text, - timestamp: m.timestamp, - isFromUser: m.isFromUser, - ), - ) - .toList(); + .._messages = messages; } static ShopInBitOrderStatus statusFromTicketState(TicketState state) { diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart index 210c45458..b240d60f3 100644 --- a/lib/pages/more_view/services_view.dart +++ b/lib/pages/more_view/services_view.dart @@ -1,10 +1,10 @@ +import 'package:drift/drift.dart' show TableOrViewStatements; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; @@ -311,11 +311,14 @@ class _ServicesViewState extends ConsumerState { onPressed: _showShopDialog, ), const SizedBox(height: 12), - Builder( - builder: (context) { - final count = MainDB.instance - .getShopInBitTickets() - .length; + StreamBuilder( + stream: ref + .watch(pSharedDrift) + .shopInBitTickets + .count() + .watchSingleOrNull(), + builder: (context, snapshot) { + final count = snapshot.data ?? 0; return SecondaryButton( label: count > 0 ? "My requests ($count)" diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 46ab31b91..5a4a3bb70 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -14,7 +14,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../../db/isar/main_db.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; @@ -311,38 +310,6 @@ class HiddenSettings extends StatelessWidget { }, ), const SizedBox(height: 12), - GestureDetector( - onTap: () async { - final tickets = MainDB.instance - .getShopInBitTickets(); - for (final t in tickets) { - await MainDB.instance.deleteShopInBitTicket( - t.ticketId, - ); - } - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: - "Deleted ${tickets.length} ShopinBit request(s)", - context: context, - ), - ); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Delete all ShopinBit requests", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), - ), - ), - const SizedBox(height: 12), Consumer( builder: (_, ref, __) { return GestureDetector( diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart index 4f88893a7..e822e8771 100644 --- a/lib/pages/shopinbit/shopinbit_car_fee_view.dart +++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart @@ -6,9 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/db/drift_provider.dart'; import '../../providers/global/shopin_bit_service_provider.dart'; import '../../services/shopinbit/src/models/address.dart'; import '../../services/shopinbit/src/models/car_research.dart'; @@ -276,7 +276,10 @@ class _ShopInBitCarFeeViewState extends ConsumerState { widget.model.isPendingPayment = true; widget.model.carResearchExpiresAt = invoice.expiresAt; widget.model.carResearchPaymentLinks = jsonEncode(invoice.paymentLinks); - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); // Best-effort fee fetch; do not block navigation on fee parse failure. await _loadFee(invoice); diff --git a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart index 38db1c73b..8ef075b70 100644 --- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app_config.dart'; -import '../../db/isar/main_db.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; @@ -484,10 +483,15 @@ class _ShopInBitCarResearchPaymentViewState widget.model.status = ShopInBitOrderStatus.pending; widget.model.isPendingPayment = false; widget.model.needsCreateRequest = false; - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); // Remove the sentinel record. if (prevTicketId != null && prevTicketId != widget.model.ticketId) { - await MainDB.instance.deleteShopInBitTicket(prevTicketId); + await (db.delete( + db.shopInBitTickets, + )..where((t) => t.ticketId.equals(prevTicketId))).go(); } if (!mounted) return; setState(() => _flowState = _PaymentFlowState.complete); @@ -550,7 +554,10 @@ class _ShopInBitCarResearchPaymentViewState // spurious list entry). widget.model.feeTicketNumber = feeResult.ticketNumber; widget.model.needsCreateRequest = true; - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); if (!mounted) return; setState(() => _flowState = _PaymentFlowState.creatingRequest); @@ -611,9 +618,13 @@ class _ShopInBitCarResearchPaymentViewState widget.model.status = ShopInBitOrderStatus.pending; widget.model.isPendingPayment = false; widget.model.needsCreateRequest = false; - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); if (prevTicketId != null && prevTicketId != widget.model.ticketId) { - await MainDB.instance.deleteShopInBitTicket(prevTicketId); + await (db.delete( + db.shopInBitTickets, + )..where((t) => t.ticketId.equals(prevTicketId))).go(); } if (!mounted) return; @@ -691,16 +702,18 @@ class _ShopInBitCarResearchPaymentViewState widget.model.status = ShopInBitOrderStatus.pending; // Flow complete: clear the resume flag before saving. widget.model.isPendingPayment = false; - await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); // Update fee receipt ticket - final feeTickets = MainDB.instance.getShopInBitTickets().where( - (t) => t.ticketId == feeTicketNumber, - ); + final feeTickets = await (db.select( + db.shopInBitTickets, + )..where((t) => t.ticketId.equals(feeTicketNumber))).get(); if (feeTickets.isNotEmpty) { - final feeTicket = feeTickets.first; - feeTicket.needsCreateRequest = false; - await MainDB.instance.putShopInBitTicket(feeTicket); + final feeTicket = feeTickets.first.copyWith(needsCreateRequest: false); + await db.into(db.shopInBitTickets).insertOnConflictUpdate(feeTicket); } if (!mounted) return; diff --git a/lib/pages/shopinbit/shopinbit_confirm_send_view.dart b/lib/pages/shopinbit/shopinbit_confirm_send_view.dart index de7bc1770..a6f85da13 100644 --- a/lib/pages/shopinbit/shopinbit_confirm_send_view.dart +++ b/lib/pages/shopinbit/shopinbit_confirm_send_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; @@ -125,7 +124,10 @@ class _ShopInBitConfirmSendViewState ? widget.tokenContract!.symbol.toUpperCase() : coin.ticker.toUpperCase(); - unawaited(MainDB.instance.putShopInBitTicket(model.toIsarTicket())); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(model.toCompanion()); // pop back to wallet if (context.mounted) { diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index d8c8123d1..7f863dd18 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -4,9 +4,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../db/isar/main_db.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/db/drift_provider.dart'; import '../../providers/global/shopin_bit_service_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; @@ -106,8 +106,11 @@ class _ShopInBitTicketDetailState extends ConsumerState { } } + final db = ref.read(pSharedDrift); unawaited( - MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()), + db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()), ); } catch (_) { // Silently fall back to local data @@ -138,8 +141,11 @@ class _ShopInBitTicketDetailState extends ConsumerState { // Reload messages from API to get accurate state await _loadFromApi(); } + final db = ref.read(pSharedDrift); unawaited( - MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket()), + db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()), ); } catch (_) { // Keep optimistic local message @@ -193,10 +199,15 @@ class _ShopInBitTicketDetailState extends ConsumerState { ..displayName = model.displayName ..requestDescription = model.requestDescription ..deliveryCountry = model.deliveryCountry; - await MainDB.instance.putShopInBitTicket(requestModel.toIsarTicket()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(requestModel.toCompanion()); model.needsCreateRequest = false; - await MainDB.instance.putShopInBitTicket(model.toIsarTicket()); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(model.toCompanion()); if (!mounted) return; setState(() => _retrying = false); diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index a7a14024a..76cfc8757 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -1,26 +1,26 @@ -import 'dart:async'; -import 'dart:convert'; +import "dart:async"; +import "dart:convert"; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; -import '../../db/isar/main_db.dart'; -import '../../models/isar/models/shopinbit_ticket.dart'; -import '../../models/shopinbit/shopinbit_order_model.dart'; -import '../../providers/global/shopin_bit_service_provider.dart'; -import '../../services/shopinbit/src/models/car_research.dart'; -import '../../themes/stack_colors.dart'; -import '../../utilities/text_styles.dart'; -import '../../utilities/util.dart'; -import '../../widgets/background.dart'; -import '../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../widgets/desktop/desktop_dialog.dart'; -import '../../widgets/desktop/desktop_dialog_close_button.dart'; -import '../../widgets/loading_indicator.dart'; -import '../../widgets/rounded_white_container.dart'; -import 'shopinbit_car_fee_view.dart'; -import 'shopinbit_car_research_payment_view.dart'; -import 'shopinbit_ticket_detail.dart'; +import "../../db/drift/shared_db/shared_database.dart"; +import "../../models/shopinbit/shopinbit_order_model.dart"; +import "../../providers/db/drift_provider.dart"; +import "../../providers/global/shopin_bit_service_provider.dart"; +import "../../services/shopinbit/src/models/car_research.dart"; +import "../../themes/stack_colors.dart"; +import "../../utilities/text_styles.dart"; +import "../../utilities/util.dart"; +import "../../widgets/background.dart"; +import "../../widgets/custom_buttons/app_bar_icon_button.dart"; +import "../../widgets/desktop/desktop_dialog.dart"; +import "../../widgets/desktop/desktop_dialog_close_button.dart"; +import "../../widgets/loading_indicator.dart"; +import "../../widgets/rounded_white_container.dart"; +import "shopinbit_car_fee_view.dart"; +import "shopinbit_car_research_payment_view.dart"; +import "shopinbit_ticket_detail.dart"; class ShopInBitTicketsView extends ConsumerStatefulWidget { const ShopInBitTicketsView({super.key}); @@ -36,36 +36,33 @@ class _ShopInBitTicketsViewState extends ConsumerState { List _tickets = []; bool _syncing = false; ShopInBitTicket? _pendingTicket; - StreamSubscription? _isarSub; + StreamSubscription>? _ticketsSub; @override void initState() { super.initState(); - _loadLocal(); - _syncFromApi(); - // Refresh on ticket writes. - _isarSub = MainDB.instance.isar.shopInBitTickets.watchLazy().listen((_) { - if (mounted) setState(_loadLocal); + final db = ref.read(pSharedDrift); + _ticketsSub = db.select(db.shopInBitTickets).watch().listen((rows) { + if (!mounted) return; + setState(() { + _pendingTicket = rows.where((t) => t.isPendingPayment).firstOrNull; + _tickets = rows + .where((t) => !t.isPendingPayment) + .map(ShopInBitOrderModel.fromDriftRow) + .toList(); + }); }); + _syncFromApi(); } @override void dispose() { - _isarSub?.cancel(); + _ticketsSub?.cancel(); super.dispose(); } - void _loadLocal() { - final allTickets = MainDB.instance.getShopInBitTickets(); - _pendingTicket = allTickets.where((t) => t.isPendingPayment).firstOrNull; - _tickets = allTickets - .where((t) => !t.isPendingPayment) - .map(ShopInBitOrderModel.fromIsarTicket) - .toList(); - } - void _resumeFlow(ShopInBitTicket pending) { - final model = ShopInBitOrderModel.fromIsarTicket(pending); + final model = ShopInBitOrderModel.fromDriftRow(pending); final expiresAt = pending.carResearchExpiresAt; final linksJson = pending.carResearchPaymentLinks; final isDesktop = Util.isDesktop; @@ -120,14 +117,16 @@ class _ShopInBitTicketsViewState extends ConsumerState { if (resp.hasError || resp.value == null) return; - for (final ref in resp.value!) { - final localIdx = _tickets.indexWhere((t) => t.apiTicketId == ref.id); + for (final ticketRef in resp.value!) { + final localIdx = _tickets.indexWhere( + (t) => t.apiTicketId == ticketRef.id, + ); if (localIdx < 0) continue; // Car research tickets return 403 on /tickets/:id/* endpoints. // if (_tickets[localIdx].category == ShopInBitCategory.car) continue; - final statusResp = await service.client.getTicketStatus(ref.id); + final statusResp = await service.client.getTicketStatus(ticketRef.id); if (statusResp.hasError || statusResp.value == null) continue; _tickets[localIdx].status = ShopInBitOrderModel.statusFromTicketState( @@ -137,7 +136,7 @@ class _ShopInBitTicketsViewState extends ConsumerState { if (_tickets[localIdx].status == ShopInBitOrderStatus.offerAvailable && (_tickets[localIdx].offerProductName == null || _tickets[localIdx].offerPrice == null)) { - final offerResp = await service.client.getTicketFull(ref.id); + final offerResp = await service.client.getTicketFull(ticketRef.id); if (!offerResp.hasError && offerResp.value != null) { _tickets[localIdx].setOffer( productName: offerResp.value!.productName, @@ -146,7 +145,7 @@ class _ShopInBitTicketsViewState extends ConsumerState { } } - final msgsResp = await service.client.getMessages(ref.id); + final msgsResp = await service.client.getMessages(ticketRef.id); if (!msgsResp.hasError && msgsResp.value != null) { _tickets[localIdx].clearMessages(); for (final m in msgsResp.value!) { @@ -160,32 +159,26 @@ class _ShopInBitTicketsViewState extends ConsumerState { } } - await MainDB.instance.putShopInBitTicket( - _tickets[localIdx].toIsarTicket(), - ); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(_tickets[localIdx].toCompanion()); } } catch (_) { - // Fall back to local data + // Fall back to local data — stream listener still has whatever was last persisted. } finally { if (mounted) { - _loadLocal(); setState(() => _syncing = false); } } } - String _categoryLabel(ShopInBitCategory? category) { - switch (category) { - case ShopInBitCategory.concierge: - return "Concierge"; - case ShopInBitCategory.travel: - return "Travel"; - case ShopInBitCategory.car: - return "Car"; - case null: - return ""; - } - } + String _categoryLabel(ShopInBitCategory? category) => switch (category) { + ShopInBitCategory.concierge => "Concierge", + ShopInBitCategory.travel => "Travel", + ShopInBitCategory.car => "Car", + null => "", + }; @override Widget build(BuildContext context) { 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 bf4da508a..9a8ff9ded 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 @@ -2,9 +2,10 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../db/isar/main_db.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/db/drift_provider.dart"; import "../../../themes/stack_colors.dart"; import "../../../utilities/text_styles.dart"; import "../../../utilities/util.dart"; @@ -24,17 +25,18 @@ const List _carConditions = ["NEW", "PREOWNED"]; const int _minCarBudget = 20000; const int _minCarFieldLength = 3; -class ShopInBitCarResearchForm extends StatefulWidget { +class ShopInBitCarResearchForm extends ConsumerStatefulWidget { const ShopInBitCarResearchForm({super.key, required this.model}); final ShopInBitOrderModel model; @override - State createState() => + ConsumerState createState() => _ShopInBitCarResearchFormState(); } -class _ShopInBitCarResearchFormState extends State { +class _ShopInBitCarResearchFormState + extends ConsumerState { final TextEditingController _brandController = TextEditingController(); final FocusNode _brandFocusNode = FocusNode(); bool _brandTouched = false; @@ -123,10 +125,10 @@ class _ShopInBitCarResearchFormState extends State { ..deliveryCountry = countryIso; // Block if another car research flow is already in progress. - final existingPending = MainDB.instance - .getShopInBitTickets() - .where((t) => t.isPendingPayment) - .toList(); + final db = ref.read(pSharedDrift); + final existingPending = await (db.select( + db.shopInBitTickets, + )..where((t) => t.isPendingPayment.equals(true))).get(); if (existingPending.isNotEmpty && mounted) { final bool? resumePrevious = await showDialog( 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 993a773d7..480f42727 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart @@ -3,6 +3,7 @@ import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/db/drift_provider.dart"; import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../utilities/util.dart"; import "shopinbit_country_picker.dart"; @@ -111,6 +112,7 @@ class _ShopInBitConciergeFormState context, widget.model, ref.read(pShopinBitService), + ref.read(pSharedDrift), ); } finally { if (mounted) setState(() => _submitting = false); diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart index 015e94d13..9fcb5b958 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart @@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; import "../../../providers/global/shopin_bit_service_provider.dart"; +import "../../../providers/providers.dart"; import "../../../utilities/util.dart"; import "shopinbit_country_picker.dart"; import "shopinbit_privacy_checkbox.dart"; @@ -70,6 +71,7 @@ class _ShopInBitGenericFormState extends ConsumerState { context, widget.model, ref.read(pShopinBitService), + ref.read(pSharedDrift), ); } finally { if (mounted) setState(() => _submitting = false); diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart index 1f0e798b9..567a5c52b 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart @@ -2,7 +2,7 @@ import "dart:async"; import "package:flutter/material.dart"; -import "../../../db/isar/main_db.dart"; +import "../../../db/drift/shared_db/shared_database.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; import "../../../notifications/show_flush_bar.dart"; import "../../../services/shopinbit/shopinbit_service.dart"; @@ -18,6 +18,7 @@ Future submitShopInBitRequest( BuildContext context, ShopInBitOrderModel model, ShopInBitService service, + SharedDatabase db, ) async { try { final String customerKey = await service.ensureCustomerKey(); @@ -64,7 +65,9 @@ Future submitShopInBitRequest( ..apiTicketId = ref.id ..ticketId = ref.number ..status = ShopInBitOrderStatus.pending; - await MainDB.instance.putShopInBitTicket(model.toIsarTicket()); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(model.toCompanion()); if (!context.mounted) return; if (Util.isDesktop) { 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 b84885a4e..a1e505f33 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart @@ -3,6 +3,7 @@ import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../models/shopinbit/shopinbit_order_model.dart"; +import "../../../providers/db/drift_provider.dart"; import "../../../providers/global/shopin_bit_service_provider.dart"; import "../../../utilities/text_styles.dart"; import "../../../utilities/util.dart"; @@ -278,6 +279,7 @@ class _ShopInBitTravelFormState extends ConsumerState { context, widget.model, ref.read(pShopinBitService), + ref.read(pSharedDrift), ); } finally { if (mounted) setState(() => _submitting = false); diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index b42fb080b..c58abbc3a 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -1,3 +1,4 @@ +import 'package:drift/drift.dart' show TableOrViewStatements; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,7 +7,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app_config.dart'; -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'; @@ -247,11 +247,15 @@ class _DesktopServicesViewState extends ConsumerState { onPressed: _showShopDialog, ), const SizedBox(width: 16), - Builder( - builder: (context) { - final count = MainDB.instance - .getShopInBitTickets() - .length; + StreamBuilder( + stream: ref + .watch(pSharedDrift) + .shopInBitTickets + .count() + .watchSingleOrNull(), + builder: (context, snapshot) { + final count = snapshot.data ?? 0; + return SecondaryButton( width: 200, buttonHeight: ButtonHeight.m, diff --git a/lib/providers/db/drift_provider.dart b/lib/providers/db/drift_provider.dart index 9f6ea4c35..efbf43649 100644 --- a/lib/providers/db/drift_provider.dart +++ b/lib/providers/db/drift_provider.dart @@ -11,7 +11,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../db/drift/database.dart' show WalletDatabase, Drift; -import '../../db/drift/shared_database.dart' show SharedDrift; +import '../../db/drift/shared_db/shared_database.dart' show SharedDrift; final pDrift = Provider.family( (ref, walletId) => Drift.get(walletId), diff --git a/lib/services/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 48db6a917..fced753af 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import '../../db/drift/shared_database.dart'; +import '../../db/drift/shared_db/shared_database.dart'; import '../../external_api_keys.dart'; import 'src/client.dart'; diff --git a/test/shopinbit/car_research_persistence_test.dart b/test/shopinbit/car_research_persistence_test.dart index 10df9aee5..b68fd5b64 100644 --- a/test/shopinbit/car_research_persistence_test.dart +++ b/test/shopinbit/car_research_persistence_test.dart @@ -61,23 +61,6 @@ void main() { }); }); - group( - 'toIsarTicket/fromIsarTicket round-trip for pending payment fields', - () { - test('isPendingPayment round-trips', () { - final model = ShopInBitOrderModel() - ..isPendingPayment = true - ..carResearchExpiresAt = DateTime(2026, 6, 1) - ..carResearchPaymentLinks = '{"BTC":"link"}'; - final ticket = model.toIsarTicket(); - final restored = ShopInBitOrderModel.fromIsarTicket(ticket); - expect(restored.isPendingPayment, isTrue); - expect(restored.carResearchExpiresAt, DateTime(2026, 6, 1)); - expect(restored.carResearchPaymentLinks, '{"BTC":"link"}'); - }); - }, - ); - group('live invoice routes to payment view', () { test('expiresAt in the future means invoice is live', () { final expiresAt = DateTime.now().add(const Duration(hours: 1)); diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 20f827852..3db3e7075 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1724,30 +1724,6 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - - @override - List<_i28.ShopInBitTicket> getShopInBitTickets() => - (super.noSuchMethod( - Invocation.method(#getShopInBitTickets, []), - returnValue: <_i28.ShopInBitTicket>[], - ) - as List<_i28.ShopInBitTicket>); - - @override - _i10.Future putShopInBitTicket(_i28.ShopInBitTicket? ticket) => - (super.noSuchMethod( - Invocation.method(#putShopInBitTicket, [ticket]), - returnValue: _i10.Future.value(0), - ) - as _i10.Future); - - @override - _i10.Future deleteShopInBitTicket(String? ticketId) => - (super.noSuchMethod( - Invocation.method(#deleteShopInBitTicket, [ticketId]), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); } /// A class which mocks [IThemeAssets]. From 0f0d582fb46ba0396e18bf5cc5704f09c08a05b3 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 12:00:56 -0600 Subject: [PATCH 19/27] refactor(ui): keep functionality the same as much as possible but refactor widget tree clean up and styling fixes --- .../shopinbit/shopinbit_ticket_detail.dart | 152 +++--- .../shopinbit/shopinbit_tickets_view.dart | 483 ++++++++---------- .../shopin_bit/desktop_shopinbit_view.dart | 4 +- ...sted_navigator_dialog_route_generator.dart | 21 + 4 files changed, 334 insertions(+), 326 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 7f863dd18..a7b89af38 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -12,11 +12,13 @@ import '../../themes/stack_colors.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/loading_indicator.dart'; +import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_offer_view.dart'; @@ -459,24 +461,34 @@ class _ShopInBitTicketDetailState extends ConsumerState { : const SizedBox.shrink(); final chatArea = Expanded( - child: Stack( - children: [ - ListView.builder( - reverse: true, - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: model.messages.length, - itemBuilder: (context, index) { - final message = model.messages[model.messages.length - 1 - index]; - return _chatBubble(message, isDesktop); - }, - ), - if (_loading) const LoadingIndicator(width: 24, height: 24), - ], + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => RoundedContainer( + padding: .zero, + color: Theme.of(context).extension()!.textFieldActiveBG, + child: child, + ), + child: Stack( + children: [ + ListView.builder( + reverse: true, + padding: const EdgeInsets.all(8), + itemCount: model.messages.length, + itemBuilder: (context, index) { + final message = + model.messages[model.messages.length - 1 - index]; + return _chatBubble(message, isDesktop); + }, + ), + // TODO: fix loading from locking everything up + if (_loading) const LoadingIndicator(width: 24, height: 24), + ], + ), ), ); final inputBar = Container( - padding: EdgeInsets.all(isDesktop ? 16 : 8), + padding: Util.isDesktop ? null : const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular(12), @@ -509,15 +521,17 @@ class _ShopInBitTicketDetailState extends ConsumerState { onSubmitted: (_) => _sendMessage(), ), ), - IconButton( - onPressed: _sendMessage, - icon: Icon( - Icons.send, - color: Theme.of( - context, - ).extension()!.accentColorBlue, + if (!Util.isDesktop) const SizedBox(width: 8), + if (!Util.isDesktop) + IconButton( + onPressed: _sendMessage, + icon: Icon( + Icons.send, + color: Theme.of( + context, + ).extension()!.accentColorBlue, + ), ), - ), ], ), ); @@ -576,51 +590,63 @@ class _ShopInBitTicketDetailState extends ConsumerState { ], ); - if (isDesktop) { - return DesktopDialog( - maxWidth: 600, - maxHeight: 650, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text("Request", style: STextStyles.desktopH3(context)), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 8, + return ConditionalParent( + condition: isDesktop, + builder: (child) => SDialog( + contentCanScroll: false, + child: SizedBox( + width: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Request", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: child, ), - child: body, ), - ), - ], - ), - ); - } - - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - model.ticketId ?? "Request", - style: STextStyles.navBarTitle(context), + ], ), ), - body: SafeArea( - child: Padding(padding: const EdgeInsets.all(16), child: body), + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + model.ticketId ?? "Request", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding(padding: const EdgeInsets.all(16), child: child), + ), + ), ), + child: body, ), ); } diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index 76cfc8757..560af5e8f 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -3,6 +3,7 @@ import "dart:convert"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_svg/flutter_svg.dart"; import "../../db/drift/shared_db/shared_database.dart"; import "../../models/shopinbit/shopinbit_order_model.dart"; @@ -10,14 +11,16 @@ import "../../providers/db/drift_provider.dart"; import "../../providers/global/shopin_bit_service_provider.dart"; import "../../services/shopinbit/src/models/car_research.dart"; import "../../themes/stack_colors.dart"; +import "../../utilities/assets.dart"; +import "../../utilities/show_loading.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/loading_indicator.dart"; -import "../../widgets/rounded_white_container.dart"; +import "../../widgets/dialogs/s_dialog.dart"; +import "../../widgets/rounded_container.dart"; import "shopinbit_car_fee_view.dart"; import "shopinbit_car_research_payment_view.dart"; import "shopinbit_ticket_detail.dart"; @@ -34,7 +37,6 @@ class ShopInBitTicketsView extends ConsumerStatefulWidget { class _ShopInBitTicketsViewState extends ConsumerState { List _tickets = []; - bool _syncing = false; ShopInBitTicket? _pendingTicket; StreamSubscription>? _ticketsSub; @@ -52,7 +54,7 @@ class _ShopInBitTicketsViewState extends ConsumerState { .toList(); }); }); - _syncFromApi(); + WidgetsBinding.instance.addPostFrameCallback((_) => _syncFromApi()); } @override @@ -109,7 +111,15 @@ class _ShopInBitTicketsViewState extends ConsumerState { } Future _syncFromApi() async { - setState(() => _syncing = true); + await showLoading( + context: context, + message: "Loading requests...", + whileFutureAlt: _syncFromApiHelper, + rootNavigator: Util.isDesktop, + ); + } + + Future _syncFromApiHelper() async { try { final service = ref.read(pShopinBitService); final customerKey = await service.ensureCustomerKey(); @@ -166,291 +176,240 @@ class _ShopInBitTicketsViewState extends ConsumerState { } } catch (_) { // Fall back to local data — stream listener still has whatever was last persisted. - } finally { - if (mounted) { - setState(() => _syncing = false); - } } } - String _categoryLabel(ShopInBitCategory? category) => switch (category) { - ShopInBitCategory.concierge => "Concierge", - ShopInBitCategory.travel => "Travel", - ShopInBitCategory.car => "Car", - null => "", - }; + static String _categoryLabel(ShopInBitCategory? category) => + switch (category) { + ShopInBitCategory.concierge => "Concierge", + ShopInBitCategory.travel => "Travel", + ShopInBitCategory.car => "Car", + null => "", + }; @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; + final pending = _pendingTicket; + final hasTickets = _tickets.isNotEmpty; - final resumeCard = _pendingTicket != null - ? GestureDetector( - onTap: () => _resumeFlow(_pendingTicket!), - child: RoundedWhiteContainer( - child: Row( + return ConditionalParent( + condition: isDesktop, + builder: (child) => SDialog( + child: SizedBox( + width: 580, + child: Column( + mainAxisSize: .min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Car Research (In Progress)", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context) - .extension()! - .accentColorYellow - .withOpacity(0.2), - ), - child: Text( - "Resume", - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle12( - context, - )) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorYellow, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - "Tap to continue your car research payment", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of( - context, - ).extension()!.textSubtitle1, - ), - ), - ], + Padding( + padding: const .only(left: 32), + child: Text( + "My requests", + style: STextStyles.desktopH3(context), ), ), - SizedBox(width: isDesktop ? 16 : 8), - Icon( - Icons.chevron_right, - color: Theme.of( - context, - ).extension()!.textSubtitle1, - ), + const DesktopDialogCloseButton(), ], ), - ), - ) - : const SizedBox.shrink(); - - final ticketList = _tickets.isEmpty - ? null - : ListView.separated( - shrinkWrap: true, - itemCount: _tickets.length, - separatorBuilder: (_, __) => SizedBox(height: isDesktop ? 16 : 12), - itemBuilder: (context, index) { - final ticket = _tickets[index]; - return GestureDetector( - onTap: () { - if (isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - builder: (_) => ShopInBitTicketDetail(model: ticket), - ); - } else { - Navigator.of(context).pushNamed( - ShopInBitTicketDetail.routeName, - arguments: ticket, - ); - } - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - ticket.ticketId ?? "N/A", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: ticket.status - .getColor( - Theme.of( - context, - ).extension()!, - ) - .withOpacity(0.2), - ), - child: Text( - ticket.status.label, - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle12( - context, - )) - .copyWith( - color: ticket.status.getColor( - Theme.of( - context, - ).extension()!, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - "${_categoryLabel(ticket.category)} \u2022 " - "${ticket.requestDescription}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle12( - context, - ).copyWith( - color: Theme.of( - context, - ).extension()!.textSubtitle1, - ), - ), - ], - ), - ), - SizedBox(width: isDesktop ? 16 : 8), - Icon( - Icons.chevron_right, - color: Theme.of( - context, - ).extension()!.textSubtitle1, - ), - ], + Flexible( + child: Padding( + padding: const .only( + left: 32, + right: 32, + bottom: 32, + top: 16, ), + child: child, ), - ); - }, - ); - - final Widget list; - if (_pendingTicket == null && _tickets.isEmpty) { - list = Center( - child: Text( - _syncing ? "Loading requests..." : "No requests yet", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), + ), + ], + ), + ), + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + "My requests", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding(padding: const .all(16), child: child), + ), + ), ), - ); - } else if (ticketList == null) { - list = resumeCard; - } else { - list = Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (_pendingTicket != null) ...[ - resumeCard, - SizedBox(height: isDesktop ? 16 : 12), - ], - ticketList, - ], - ); - } - - final content = Stack( - children: [ - list, - if (_syncing) const LoadingIndicator(width: 24, height: 24), - ], - ); - - if (isDesktop) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 550, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: .min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "My requests", - style: STextStyles.desktopH3(context), + if (pending == null && !hasTickets) + Center( + child: Text( + "No requests yet", + style: Util.isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle(context), + ), + ) + else ...[ + if (pending != null) ...[ + RoundedContainer( + color: Theme.of(context).extension()!.popupBG, + onPressed: () => _resumeFlow(pending), + child: _RequestRow( + title: "Car Research (In Progress)", + subtitle: "Tap to continue your car research payment", + badgeText: "Resume", + badgeColor: Theme.of( + context, + ).extension()!.accentColorYellow, ), ), - const DesktopDialogCloseButton(), + if (hasTickets) SizedBox(height: isDesktop ? 16 : 12), ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, + if (hasTickets) + ListView.separated( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _tickets.length, + separatorBuilder: (_, __) => + SizedBox(height: isDesktop ? 16 : 12), + itemBuilder: (context, index) { + final ticket = _tickets[index]; + + return RoundedContainer( + padding: .all(Util.isDesktop ? 16 : 12), + borderColor: Util.isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, + color: Theme.of( + context, + ).extension()!.popupBG, + onPressed: () => Navigator.of(context).pushNamed( + ShopInBitTicketDetail.routeName, + arguments: ticket, + ), + child: _RequestRow( + title: ticket.ticketId ?? "N/A", + subtitle: + "${_categoryLabel(ticket.category)} \u2022 ${ticket.requestDescription}", + badgeText: ticket.status.label, + badgeColor: ticket.status.getColor( + Theme.of(context).extension()!, + ), + ), + ); + }, ), - child: content, - ), - ), + + // // TODO: fix loading from locking everything up + // if (_syncing) const LoadingIndicator(width: 24, height: 24), + ], ], ), - ); - } + ), + ); + } +} + +class _RequestRow extends StatelessWidget { + const _RequestRow({ + required this.title, + required this.subtitle, + required this.badgeText, + required this.badgeColor, + }); + + final String title; + final String subtitle; + final String badgeText; + final Color badgeColor; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + final stackColors = Theme.of(context).extension()!; - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () => Navigator.of(context).pop(), + final titleStyle = isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context); + + final subtitleStyle = isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12( + context, + ).copyWith(color: stackColors.textSubtitle1); + + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: titleStyle), + _StatusBadge(text: badgeText, color: badgeColor), + ], + ), + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: subtitleStyle, + ), + ], ), - title: Text("My requests", style: STextStyles.navBarTitle(context)), ), - body: SafeArea( - child: Padding(padding: const EdgeInsets.all(16), child: content), + SizedBox(width: isDesktop ? 16 : 8), + SvgPicture.asset( + Assets.svg.chevronRight, + width: 14, + colorFilter: ColorFilter.mode(stackColors.textSubtitle1, .srcIn), ), + ], + ); + } +} + +class _StatusBadge extends StatelessWidget { + const _StatusBadge({required this.text, required this.color}); + + final String text; + final Color color; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + final style = + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context)) + .copyWith(color: color); + + return Container( + padding: const .symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: color.withOpacity(0.2), ), + child: Text(text, style: style), ); } } diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index c58abbc3a..1f2b62da0 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -265,7 +265,9 @@ class _DesktopServicesViewState extends ConsumerState { onPressed: () async { await showDialog( context: context, - builder: (_) => const ShopInBitTicketsView(), + builder: (_) => const NestedNavigatorDialog( + initialRoute: ShopInBitTicketsView.routeName, + ), ); if (mounted) setState(() {}); }, 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 7d924861b..a5ce6ab5b 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 @@ -7,6 +7,8 @@ import '../../../pages/shopinbit/shopinbit_step_1.dart'; import '../../../pages/shopinbit/shopinbit_step_2.dart'; import '../../../pages/shopinbit/shopinbit_step_3.dart'; import '../../../pages/shopinbit/shopinbit_step_4.dart'; +import '../../../pages/shopinbit/shopinbit_ticket_detail.dart'; +import '../../../pages/shopinbit/shopinbit_tickets_view.dart'; import '../../../pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -84,6 +86,25 @@ abstract final class NestedNavigatorDialogRouteGenerator { "Expected ShopInBitOrderModel", ); + case ShopInBitTicketsView.routeName: + return getRoute( + builder: (_) => const ShopInBitTicketsView(), + settings: RouteSettings(name: settings.name), + ); + + case ShopInBitTicketDetail.routeName: + if (args is ShopInBitOrderModel) { + return getRoute( + builder: (_) => ShopInBitTicketDetail(model: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected ShopInBitOrderModel", + ); + default: return _routeError("Unknown route name: ${settings.name}"); } From f1b14f11f516d0b93b0db5e61afcbb454e4a7d05 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 12:19:49 -0600 Subject: [PATCH 20/27] fix(ui): chat bubble colors --- lib/pages/shopinbit/shopinbit_ticket_detail.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index a7b89af38..2a6cb4147 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -312,7 +312,9 @@ class _ShopInBitTicketDetailState extends ConsumerState { } Widget _chatBubble(ShopInBitMessage message, bool isDesktop) { - final textColor = message.isFromUser ? Colors.white : null; + final textColor = message.isFromUser + ? Theme.of(context).extension()!.buttonTextPrimary + : Theme.of(context).extension()!.buttonTextSecondary; return Align( alignment: message.isFromUser @@ -324,8 +326,8 @@ class _ShopInBitTicketDetailState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: message.isFromUser - ? Theme.of(context).extension()!.accentColorBlue - : Theme.of(context).extension()!.popupBG, + ? Theme.of(context).extension()!.buttonBackPrimary + : Theme.of(context).extension()!.buttonBackSecondary, borderRadius: BorderRadius.only( topLeft: const Radius.circular(12), topRight: const Radius.circular(12), From 5fa4bcc104c0dd704f33ff841b8f5931b5756724 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 12:24:37 -0600 Subject: [PATCH 21/27] fix(ui): don't display UTC time --- lib/pages/shopinbit/shopinbit_ticket_detail.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 2a6cb4147..5e8880256 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -237,6 +237,11 @@ class _ShopInBitTicketDetailState extends ConsumerState { } String _formatTime(DateTime dt) { + // TODO: local time is a start but this is still far from ideal... + if (dt.isUtc) { + dt = dt.toLocal(); + } + final hour = dt.hour.toString().padLeft(2, '0'); final minute = dt.minute.toString().padLeft(2, '0'); return "$hour:$minute"; From beed5b62b02625bf798e24eabb833017cd249267 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 12:31:51 -0600 Subject: [PATCH 22/27] fix(ui): adjust button width to prevent overflow on min window width --- .../services/shopin_bit/desktop_shopinbit_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index 1f2b62da0..85d1a19f0 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -240,7 +240,7 @@ class _DesktopServicesViewState extends ConsumerState { child: Row( children: [ PrimaryButton( - width: 250, + width: 224, buttonHeight: ButtonHeight.m, enabled: true, label: "Shop with ShopinBit", @@ -257,7 +257,7 @@ class _DesktopServicesViewState extends ConsumerState { final count = snapshot.data ?? 0; return SecondaryButton( - width: 200, + width: 196, buttonHeight: ButtonHeight.m, label: count > 0 ? "My requests ($count)" @@ -276,7 +276,7 @@ class _DesktopServicesViewState extends ConsumerState { ), const SizedBox(width: 16), SecondaryButton( - width: 140, + width: 118, buttonHeight: ButtonHeight.m, label: "Settings", onPressed: () { From 2c81a6747ac74a300a234a2d66070b21852ab583 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 13:27:40 -0600 Subject: [PATCH 23/27] fix(ui): cakepay desktop navigation mostly --- .../cakepay/cakepay_card_detail_view.dart | 15 +- lib/pages/cakepay/cakepay_order_view.dart | 204 +++++------ lib/pages/cakepay/cakepay_orders_view.dart | 319 +++++++++--------- lib/pages/cakepay/cakepay_vendors_view.dart | 140 ++++---- .../shopinbit/shopinbit_tickets_view.dart | 3 +- .../cakepay/desktop_gift_cards_view.dart | 9 +- lib/route_generator.dart | 5 +- ...sted_navigator_dialog_route_generator.dart | 44 +++ 8 files changed, 362 insertions(+), 377 deletions(-) diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 07ec3d603..0121b248d 100644 --- a/lib/pages/cakepay/cakepay_card_detail_view.dart +++ b/lib/pages/cakepay/cakepay_card_detail_view.dart @@ -193,18 +193,9 @@ class _CakePayCardDetailViewState extends State { await CakePayService.instance.addOrderId(order.orderId); if (mounted) { - if (Util.isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - await showDialog( - context: context, - builder: (_) => CakePayOrderView(orderId: order.orderId), - ); - } else { - await Navigator.of(context).pushReplacementNamed( - CakePayOrderView.routeName, - arguments: order.orderId, - ); - } + await Navigator.of( + context, + ).pushReplacementNamed(CakePayOrderView.routeName, arguments: order); } } else { final String errorMessage; diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart index 62fc0bd41..7488d0e99 100644 --- a/lib/pages/cakepay/cakepay_order_view.dart +++ b/lib/pages/cakepay/cakepay_order_view.dart @@ -14,6 +14,7 @@ import '../../services/cakepay/src/models/order.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/assets.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -23,25 +24,24 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/dialogs/s_dialog.dart'; -import '../../widgets/loading_indicator.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; +import '../wallet_view/transaction_views/transaction_details_view.dart'; import 'cakepay_send_from_view.dart'; class CakePayOrderView extends ConsumerStatefulWidget { - const CakePayOrderView({super.key, required this.orderId}); + const CakePayOrderView({super.key, required this.order}); static const String routeName = "/cakePayOrder"; - final String orderId; + final CakePayOrder order; @override ConsumerState createState() => _CakePayOrderViewState(); } class _CakePayOrderViewState extends ConsumerState { - CakePayOrder? _order; - bool _loading = true; + late CakePayOrder _order; Timer? _pollTimer; Timer? _countdownTimer; Duration _timeRemaining = Duration.zero; @@ -50,7 +50,11 @@ class _CakePayOrderViewState extends ConsumerState { @override void initState() { super.initState(); - _loadOrder(); + _order = widget.order; + + // TODO: _loadOrder already locked up the ui previously, this just puts a + // nicer loading ui in place + WidgetsBinding.instance.addPostFrameCallback((_) => _loadOrder()); _pollTimer = Timer.periodic( const Duration(seconds: 15), (_) => _loadOrder(), @@ -74,9 +78,9 @@ class _CakePayOrderViewState extends ConsumerState { } void _updateTimeRemaining() { - if (_order?.expirationTime == null) return; + if (_order.expirationTime == null) return; final expiresAt = DateTime.fromMillisecondsSinceEpoch( - _order!.expirationTime!, + _order.expirationTime!, ); final remaining = expiresAt.difference(DateTime.now()); if (mounted) { @@ -107,7 +111,6 @@ class _CakePayOrderViewState extends ConsumerState { }) { final isDesktop = Util.isDesktop; if (isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); showDialog( context: context, builder: (_) => CakePaySendFromView( @@ -211,16 +214,26 @@ class _CakePayOrderViewState extends ConsumerState { } Future _loadOrder() async { - final resp = await CakePayService.instance.client.getOrder(widget.orderId); + await showLoading( + context: context, + message: "Updating order...", + whileFutureAlt: _loadOrderHelper, + rootNavigator: Util.isDesktop, + ); + } + + Future _loadOrderHelper() async { + final resp = await CakePayService.instance.client.getOrder( + widget.order.orderId, + ); if (mounted) { setState(() { - _loading = false; if (!resp.hasError && resp.value != null) { _order = resp.value!; - if (_isTerminal(_order!.status)) { + if (_isTerminal(_order.status)) { _pollTimer?.cancel(); _countdownTimer?.cancel(); - } else if (_order!.expirationTime != null) { + } else if (_order.expirationTime != null) { _startCountdown(); } } @@ -266,42 +279,34 @@ class _CakePayOrderViewState extends ConsumerState { return [ // Copyable order ID. RoundedWhiteContainer( - child: GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: order.orderId)); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Order ID copied", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Order ID", style: subtitleStyle), - const SizedBox(height: 4), - Text( - order.orderId, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - ], - ), - ), - Icon( - Icons.copy, - size: 14, - color: Theme.of( - context, - ).extension()!.accentColorBlue, + onPressed: () { + Clipboard.setData(ClipboardData(text: order.orderId)); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Order ID copied", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order ID", style: subtitleStyle), + const SizedBox(height: 4), + Text( + order.orderId, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), + ), + ], ), - ], - ), + ), + IconCopyButton(data: order.orderId), + ], ), ), // Created-at timestamp. @@ -324,28 +329,7 @@ class _CakePayOrderViewState extends ConsumerState { Widget build(BuildContext context) { final isDesktop = Util.isDesktop; - if (_loading) { - return _scaffold( - isDesktop: isDesktop, - child: const LoadingIndicator(width: 24, height: 24), - ); - } - - if (_order == null) { - return _scaffold( - isDesktop: isDesktop, - child: Center( - child: Text( - "Failed to load order", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - ), - ); - } - - final order = _order!; + final order = _order; final paymentOptions = order.paymentOptions; final details = [ @@ -377,46 +361,38 @@ class _CakePayOrderViewState extends ConsumerState { ), SizedBox(height: isDesktop ? 8 : 6), RoundedWhiteContainer( - child: GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: order.orderId)); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Order ID copied", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Order ID", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SelectableText( - order.orderId, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - const SizedBox(width: 6), - Icon( - Icons.copy, - size: 14, - color: Theme.of( - context, - ).extension()!.accentColorBlue, - ), - ], - ), - ], - ), + onPressed: () { + Clipboard.setData(ClipboardData(text: order.orderId)); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Order ID copied", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Order ID", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SelectableText( + order.orderId, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), + ), + const SizedBox(width: 6), + IconCopyButton(data: order.orderId), + ], + ), + ], ), ), SizedBox(height: isDesktop ? 16 : 12), @@ -833,13 +809,7 @@ class _CakePayOrderViewState extends ConsumerState { : STextStyles.itemSubtitle12(context), ), const Spacer(), - Icon( - Icons.copy, - size: 14, - color: Theme.of( - context, - ).extension()!.accentColorBlue, - ), + IconCopyButton(data: order.orderId), const SizedBox(width: 4), Text("Copy", style: STextStyles.link2(context)), ], diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index 48b966507..f808fcf7b 100644 --- a/lib/pages/cakepay/cakepay_orders_view.dart +++ b/lib/pages/cakepay/cakepay_orders_view.dart @@ -3,15 +3,15 @@ import 'package:flutter/material.dart'; import '../../services/cakepay/cakepay_service.dart'; import '../../services/cakepay/src/models/order.dart'; import '../../themes/stack_colors.dart'; +import '../../utilities/show_loading.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/loading_indicator.dart'; -import '../../widgets/rounded_white_container.dart'; +import '../../widgets/dialogs/s_dialog.dart'; +import '../../widgets/rounded_container.dart'; import 'cakepay_order_view.dart'; class CakePayOrdersView extends StatefulWidget { @@ -25,19 +25,23 @@ class CakePayOrdersView extends StatefulWidget { class _CakePayOrdersViewState extends State { List _orders = []; - bool _syncing = false; @override void initState() { super.initState(); - _syncFromApi(); + WidgetsBinding.instance.addPostFrameCallback((_) => _syncFromApi()); } - /// Fetch each locally-tracked order ID individually via getOrder() - /// (which works with the seller API key, unlike getMyOrders()). - /// Mirrors ShopInBit's _syncFromApi() pattern. Future _syncFromApi() async { - setState(() => _syncing = true); + await showLoading( + context: context, + message: "Loading orders...", + whileFutureAlt: _syncFromApiHelper, + rootNavigator: Util.isDesktop, + ); + } + + Future _syncFromApiHelper() async { try { final orderIds = await CakePayService.instance.getOrderIds(); final results = []; @@ -56,10 +60,6 @@ class _CakePayOrdersViewState extends State { } } catch (_) { // Fall back to empty list — no local cache to fall back on - } finally { - if (mounted) { - setState(() => _syncing = false); - } } } @@ -67,162 +67,35 @@ class _CakePayOrdersViewState extends State { Widget build(BuildContext context) { final isDesktop = Util.isDesktop; - final list = _orders.isEmpty - ? Center( - child: Text( - _syncing ? "Loading orders..." : "No orders yet", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle(context), - ), - ) - : ListView.separated( - shrinkWrap: isDesktop, - primary: isDesktop ? false : null, - itemCount: _orders.length, - separatorBuilder: (_, __) => SizedBox(height: isDesktop ? 16 : 12), - itemBuilder: (context, index) { - final order = _orders[index]; - return GestureDetector( - onTap: () { - if (isDesktop) { - Navigator.of(context, rootNavigator: true).pop(); - showDialog( - context: context, - builder: (_) => CakePayOrderView(orderId: order.orderId), - ); - } else { - Navigator.of(context).pushNamed( - CakePayOrderView.routeName, - arguments: order.orderId, - ); - } - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - order.orderId.length > 8 - ? "${order.orderId.substring(0, 8)}..." - : order.orderId, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: order.status - .color( - Theme.of( - context, - ).extension()!, - ) - .withValues(alpha: 0.2), - ), - child: Text( - order.status.label, - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle12( - context, - )) - .copyWith( - color: order.status.color( - Theme.of( - context, - ).extension()!, - ), - ), - ), - ), - ], - ), - if (order.amountUsd != null) ...[ - const SizedBox(height: 4), - Text( - "\$${order.amountUsd} USD", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle12( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ], - ), - ), - SizedBox(width: isDesktop ? 16 : 8), - Icon( - Icons.chevron_right, - color: Theme.of( - context, - ).extension()!.textSubtitle1, - ), - ], - ), - ), - ); - }, - ); - - final content = Stack( - children: [ - list, - if (_syncing) const LoadingIndicator(width: 24, height: 24), - ], - ); - return ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: 550, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "My Orders", - 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( + "My Orders", + 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: 16, - ), - child: child, ), - ), - ], + ], + ), ), ), child: ConditionalParent( @@ -243,7 +116,123 @@ class _CakePayOrdersViewState extends State { ), ), ), - child: content, + child: _orders.isEmpty + ? Center( + child: Text( + "No orders yet", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle(context), + ), + ) + : ListView.separated( + shrinkWrap: isDesktop, + primary: isDesktop ? false : null, + itemCount: _orders.length, + padding: isDesktop ? const .only(bottom: 32, top: 16) : null, + separatorBuilder: (_, __) => + SizedBox(height: isDesktop ? 16 : 12), + itemBuilder: (context, index) { + final order = _orders[index]; + return RoundedContainer( + padding: .all(Util.isDesktop ? 16 : 12), + borderColor: Util.isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, + color: Theme.of(context).extension()!.popupBG, + onPressed: () { + Navigator.of( + context, + ).pushNamed(CakePayOrderView.routeName, arguments: order); + }, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + order.orderId.length > 8 + ? "${order.orderId.substring(0, 8)}..." + : order.orderId, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: order.status + .color( + Theme.of( + context, + ).extension()!, + ) + .withValues(alpha: 0.2), + ), + child: Text( + order.status.label, + style: + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle12( + context, + )) + .copyWith( + color: order.status.color( + Theme.of( + context, + ).extension()!, + ), + ), + ), + ), + ], + ), + if (order.amountUsd != null) ...[ + const SizedBox(height: 4), + Text( + "\$${order.amountUsd} USD", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle12( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ], + ), + ), + SizedBox(width: isDesktop ? 16 : 8), + Icon( + Icons.chevron_right, + color: Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/pages/cakepay/cakepay_vendors_view.dart b/lib/pages/cakepay/cakepay_vendors_view.dart index 5c16cdd7d..d04f05baf 100644 --- a/lib/pages/cakepay/cakepay_vendors_view.dart +++ b/lib/pages/cakepay/cakepay_vendors_view.dart @@ -17,7 +17,7 @@ import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/icon_widgets/credit_card_icon.dart'; import '../../widgets/loading_indicator.dart'; -import '../../widgets/rounded_white_container.dart'; +import '../../widgets/rounded_container.dart'; import '../../widgets/stack_text_field.dart'; import 'cakepay_card_detail_view.dart'; @@ -98,21 +98,9 @@ class _CakePayVendorsViewState extends State { } Future _onCardTapped(CakePayCard card) async { - if (Util.isDesktop) { - // this pop makes going back annoying as the whole list needs to be - // searched again with API calls etc. Leaving in for now as this is how I - // found it and removing here could introduce worse issues somewhere else. - Navigator.of(context, rootNavigator: true).pop(); - - await showDialog( - context: context, - builder: (_) => CakePayCardDetailView(card: card), - ); - } else { - await Navigator.of( - context, - ).pushNamed(CakePayCardDetailView.routeName, arguments: card); - } + await Navigator.of( + context, + ).pushNamed(CakePayCardDetailView.routeName, arguments: card); } @override @@ -143,10 +131,7 @@ class _CakePayVendorsViewState extends State { ), Flexible( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 8, - ), + padding: const .only(left: 32, right: 32, top: 8), child: child, ), ), @@ -213,9 +198,7 @@ class _CakePayVendorsViewState extends State { shrinkWrap: isDesktop, primary: isDesktop ? false : null, itemCount: cards.length, - padding: isDesktop - ? null - : const EdgeInsets.only(bottom: 16), + padding: .only(bottom: isDesktop ? 32 : 16), separatorBuilder: (_, __) => SizedBox(height: isDesktop ? 16 : 12), itemBuilder: (_, index) => _CardTile( @@ -416,66 +399,67 @@ class _CardTile extends StatelessWidget { final isDesktop = Util.isDesktop; final colors = Theme.of(context).extension()!; - return GestureDetector( - onTap: onTap, - child: RoundedWhiteContainer( - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: card.cardImageUrl != null - ? Image.network( - card.cardImageUrl!, - width: isDesktop ? 60 : 48, - height: isDesktop ? 40 : 32, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => CreditCardIcon( - width: isDesktop ? 40 : 32, - height: isDesktop ? 40 : 32, - ), - ) - : CreditCardIcon( + return RoundedContainer( + color: colors.popupBG, + borderColor: isDesktop ? colors.textFieldDefaultBG : null, + onPressed: onTap, + padding: isDesktop ? const .all(16) : const .all(12), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: card.cardImageUrl != null + ? Image.network( + card.cardImageUrl!, + width: isDesktop ? 60 : 48, + height: isDesktop ? 40 : 32, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => CreditCardIcon( width: isDesktop ? 40 : 32, height: isDesktop ? 40 : 32, ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - card.name, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ) + : CreditCardIcon( + width: isDesktop ? 40 : 32, + height: isDesktop ? 40 : 32, ), - const SizedBox(height: 2), - Text( - [ - if (card.denominationRange.isNotEmpty) - card.denominationRange, - if (card.currencyCode != null) card.currencyCode!, - ].join(' '), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle1), - ), - ], - ), - ), - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - colorFilter: ColorFilter.mode(colors.textSubtitle1, .srcIn), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + card.name, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + [ + if (card.denominationRange.isNotEmpty) + card.denominationRange, + if (card.currencyCode != null) card.currencyCode!, + ].join(' '), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12( + context, + ).copyWith(color: colors.textSubtitle1), + ), + ], ), - ], - ), + ), + SvgPicture.asset( + Assets.svg.chevronRight, + width: 20, + height: 20, + colorFilter: ColorFilter.mode(colors.textSubtitle1, .srcIn), + ), + ], ), ); } diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index 560af5e8f..c71782a79 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -380,7 +380,8 @@ class _RequestRow extends StatelessWidget { SizedBox(width: isDesktop ? 16 : 8), SvgPicture.asset( Assets.svg.chevronRight, - width: 14, + width: 20, + height: 20, colorFilter: ColorFilter.mode(stackColors.textSubtitle1, .srcIn), ), ], diff --git a/lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart b/lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart index 964028acb..22b077a9c 100644 --- a/lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart +++ b/lib/pages_desktop_specific/services/cakepay/desktop_gift_cards_view.dart @@ -10,6 +10,7 @@ import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart'; import '../../../widgets/icon_widgets/credit_card_icon.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/tor_subscription.dart'; @@ -105,7 +106,9 @@ class _DesktopGiftCardsViewState extends ConsumerState { onPressed: () { showDialog( context: context, - builder: (_) => const CakePayVendorsView(), + builder: (_) => const NestedNavigatorDialog( + initialRoute: CakePayVendorsView.routeName, + ), ); }, ), @@ -118,7 +121,9 @@ class _DesktopGiftCardsViewState extends ConsumerState { onPressed: () { showDialog( context: context, - builder: (_) => const CakePayOrdersView(), + builder: (_) => const NestedNavigatorDialog( + initialRoute: CakePayOrdersView.routeName, + ), ); }, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5aa23961d..2a42a8af7 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -258,6 +258,7 @@ import 'pages_desktop_specific/settings/settings_menu/syncing_preferences_settin import 'pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart'; import 'pages_desktop_specific/spark_coins/spark_coins_view.dart'; import 'services/cakepay/src/models/card.dart'; +import 'services/cakepay/src/models/order.dart'; import 'services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'services/shopinbit/src/models/car_research.dart'; @@ -1105,10 +1106,10 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case CakePayOrderView.routeName: - if (args is String) { + if (args is CakePayOrder) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CakePayOrderView(orderId: args), + builder: (_) => CakePayOrderView(order: args), settings: RouteSettings(name: settings.name), ); } 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 a5ce6ab5b..3aa7d816b 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 @@ -3,6 +3,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../../models/shopinbit/shopinbit_order_model.dart'; +import '../../../pages/cakepay/cakepay_card_detail_view.dart'; +import '../../../pages/cakepay/cakepay_order_view.dart'; +import '../../../pages/cakepay/cakepay_orders_view.dart'; +import '../../../pages/cakepay/cakepay_vendors_view.dart'; import '../../../pages/shopinbit/shopinbit_step_1.dart'; import '../../../pages/shopinbit/shopinbit_step_2.dart'; import '../../../pages/shopinbit/shopinbit_step_3.dart'; @@ -10,6 +14,8 @@ import '../../../pages/shopinbit/shopinbit_step_4.dart'; import '../../../pages/shopinbit/shopinbit_ticket_detail.dart'; import '../../../pages/shopinbit/shopinbit_tickets_view.dart'; import '../../../pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart'; +import '../../../services/cakepay/src/models/card.dart'; +import '../../../services/cakepay/src/models/order.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../conditional_parent.dart'; @@ -105,6 +111,44 @@ abstract final class NestedNavigatorDialogRouteGenerator { "Expected ShopInBitOrderModel", ); + case CakePayVendorsView.routeName: + return getRoute( + builder: (_) => const CakePayVendorsView(), + settings: RouteSettings(name: settings.name), + ); + + case CakePayOrdersView.routeName: + return getRoute( + builder: (_) => const CakePayOrdersView(), + settings: RouteSettings(name: settings.name), + ); + + case CakePayCardDetailView.routeName: + if (args is CakePayCard) { + return getRoute( + builder: (_) => CakePayCardDetailView(card: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected CakePayCard", + ); + + case CakePayOrderView.routeName: + if (args is CakePayOrder) { + return getRoute( + builder: (_) => CakePayOrderView(order: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError( + "${settings.name} invalid args\n" + "Got ${args.runtimeType}\n" + "Expected CakePayOrder", + ); + default: return _routeError("Unknown route name: ${settings.name}"); } From 73269aff316bf247065fd582edfd5d9eb177bcbd Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 14:56:08 -0600 Subject: [PATCH 24/27] fix(ui): provider access after widget disposed --- lib/pages/shopinbit/shopinbit_settings_view.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_settings_view.dart b/lib/pages/shopinbit/shopinbit_settings_view.dart index 51fb674d5..a4b36b2ab 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -54,12 +54,14 @@ class _ShopInBitSettingsViewState extends ConsumerState { .read(pSharedDrift) .shopinBitSettingsDao .getSettings(); - final key = await ref.read(pShopinBitService).loadCustomerKey(); if (mounted) { - setState(() { - _currentKey = key; - _displayNameController.text = settings.displayName ?? ""; - }); + final key = await ref.read(pShopinBitService).loadCustomerKey(); + if (mounted) { + setState(() { + _currentKey = key; + _displayNameController.text = settings.displayName ?? ""; + }); + } } }(); } From cbad601b54197a014dce18ae7227143bd8294ba2 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 16:16:38 -0600 Subject: [PATCH 25/27] fix(ui): clean up as much as possible without fully refactoring --- .../shopinbit/shopinbit_ticket_detail.dart | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 5e8880256..9875a7bdd 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -386,45 +386,56 @@ class _ShopInBitTicketDetailState extends ConsumerState { final isDesktop = Util.isDesktop; final model = widget.model; - final statusBar = RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - model.ticketId ?? "Request", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.titleBold12(context), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: model.status - .getColor(Theme.of(context).extension()!) - .withOpacity(0.2), + final statusBar = Padding( + padding: .only(bottom: isDesktop ? 12 : 8), + child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SelectableText( + model.ticketId ?? "Request", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.titleBold12(context), ), - child: Text( - model.status.label, - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context)) - .copyWith( - color: model.status.getColor( - Theme.of(context).extension()!, + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: model.status + .getColor(Theme.of(context).extension()!) + .withOpacity(0.2), + ), + child: Text( + model.status.label, + style: + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context)) + .copyWith( + color: model.status.getColor( + Theme.of(context).extension()!, + ), ), - ), + ), ), - ), - ], + ], + ), ), ); final offerBanner = model.status == ShopInBitOrderStatus.offerAvailable ? Padding( - padding: EdgeInsets.only(bottom: isDesktop ? 16 : 12), + padding: .only(bottom: isDesktop ? 12 : 8), child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -494,12 +505,9 @@ class _ShopInBitTicketDetailState extends ConsumerState { ), ); - final inputBar = Container( - padding: Util.isDesktop ? null : const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular(12), - ), + final inputBar = RoundedContainer( + padding: Util.isDesktop ? .zero : const .all(8), + color: Theme.of(context).extension()!.popupBG, child: Row( children: [ Expanded( @@ -548,6 +556,11 @@ class _ShopInBitTicketDetailState extends ConsumerState { ? Padding( padding: EdgeInsets.only(bottom: isDesktop ? 12 : 8), child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -558,7 +571,7 @@ class _ShopInBitTicketDetailState extends ConsumerState { : STextStyles.titleBold12(context), ), const SizedBox(height: 8), - Text( + SelectableText( model.requestDescription, style: isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) @@ -586,6 +599,8 @@ class _ShopInBitTicketDetailState extends ConsumerState { : const SizedBox.shrink(); final body = Column( + mainAxisSize: .min, + crossAxisAlignment: .stretch, children: [ statusBar, retryButton, From f2c96ae55f6b02ccd08332b25b484ac14e021ef3 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 16:59:22 -0600 Subject: [PATCH 26/27] fix(ui): more clean up and tweaks --- .../cakepay/cakepay_card_detail_view.dart | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 0121b248d..53f2ce5b7 100644 --- a/lib/pages/cakepay/cakepay_card_detail_view.dart +++ b/lib/pages/cakepay/cakepay_card_detail_view.dart @@ -17,6 +17,7 @@ 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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../../widgets/textfields/adaptive_text_field.dart'; @@ -360,7 +361,7 @@ class _CakePayCardDetailViewState extends State { : null, onIncrement: () => setState(() => _quantity++), ), - SizedBox(height: isDesktop ? 16 : 12), + SizedBox(height: isDesktop ? 24 : 16), _TermsCheckbox( isDesktop: isDesktop, accepted: _termsAccepted, @@ -370,7 +371,7 @@ class _CakePayCardDetailViewState extends State { }, onOpenTerms: _openTerms, ), - SizedBox(height: isDesktop ? 16 : 12), + SizedBox(height: isDesktop ? 24 : 16), Text( "Email for receipt and delivery", style: isDesktop @@ -407,18 +408,33 @@ class _CardImage extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( + return ConditionalParent( + condition: isDesktop, + builder: (child) => Padding( + padding: const .symmetric(vertical: 8), + child: Center(child: child), + ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( imageUrl, - width: isDesktop ? 200 : 150, - fit: BoxFit.contain, - errorBuilder: (BuildContext _, Object __, StackTrace? ___) => - CreditCardIcon( + width: isDesktop ? 300 : null, + fit: isDesktop ? .contain : .fitWidth, + loadingBuilder: (_, child, event) { + if (event != null) { + return LoadingIndicator( width: isDesktop ? 80 : 60, height: isDesktop ? 80 : 60, - ), + ); + } + return child; + }, + errorBuilder: (BuildContext _, Object __, StackTrace? ___) => Center( + child: CreditCardIcon( + width: isDesktop ? 80 : 60, + height: isDesktop ? 80 : 60, + ), + ), ), ), ); @@ -434,6 +450,10 @@ class _PlainInfoBlock extends StatelessWidget { @override Widget build(BuildContext context) { return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, + padding: isDesktop ? const .all(16) : const .all(12), child: Text( text, style: isDesktop @@ -458,6 +478,10 @@ class _TitledInfoBlock extends StatelessWidget { @override Widget build(BuildContext context) { return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, + padding: isDesktop ? const .all(16) : const .all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From e5d418f0903f9a5cd8aa5e19360dfa654f3e3875 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 May 2026 19:06:12 -0600 Subject: [PATCH 27/27] refactor(ui): generalized external link launch request dialog --- .../cakepay/cakepay_card_detail_view.dart | 98 +-------------- lib/pages/more_view/services_view.dart | 79 +++--------- .../shopinbit_privacy_checkbox.dart | 47 +------- .../shopin_bit/desktop_shopinbit_view.dart | 74 +----------- ...quest_external_link_navigation_dialog.dart | 114 ++++++++++++++++++ 5 files changed, 142 insertions(+), 270 deletions(-) create mode 100644 lib/widgets/dialogs/request_external_link_navigation_dialog.dart diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 53f2ce5b7..09a205c45 100644 --- a/lib/pages/cakepay/cakepay_card_detail_view.dart +++ b/lib/pages/cakepay/cakepay_card_detail_view.dart @@ -1,7 +1,6 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../services/cakepay/cakepay_service.dart'; import '../../services/cakepay/src/models/card.dart'; @@ -11,10 +10,9 @@ 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/desktop/secondary_button.dart'; +import '../../widgets/dialogs/request_external_link_navigation_dialog.dart'; import '../../widgets/dialogs/s_dialog.dart'; import '../../widgets/icon_widgets/credit_card_icon.dart'; import '../../widgets/loading_indicator.dart'; @@ -75,101 +73,9 @@ class _CakePayCardDetailViewState extends State { return true; } - Future _showOpenBrowserWarning(String url) async { - final uri = Uri.parse(url); - final shouldContinue = await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => Util.isDesktop - ? DesktopDialog( - maxWidth: 550, - maxHeight: 250, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 20, - ), - child: Column( - children: [ - Text("Attention", style: STextStyles.desktopH2(context)), - const SizedBox(height: 16), - Text( - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 35), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); - }, - ), - const SizedBox(width: 20), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); - }, - ), - ], - ), - ], - ), - ), - ) - : StackDialog( - title: "Attention", - message: - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text("Continue", style: STextStyles.button(context)), - ), - ), - ); - return shouldContinue ?? false; - } - Future _openTerms() async { const url = "https://cakepay.com/terms/"; - if (await _showOpenBrowserWarning(url)) { - await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); - } + await showRequestExternalLinkAndMaybeLaunch(context, uri: Uri.parse(url)); } Future _purchase() async { diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart index b240d60f3..403ac0bf3 100644 --- a/lib/pages/more_view/services_view.dart +++ b/lib/pages/more_view/services_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../models/shopinbit/shopinbit_order_model.dart'; import '../../providers/providers.dart'; @@ -14,6 +13,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/dialogs/request_external_link_navigation_dialog.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../shopinbit/shopinbit_settings_view.dart'; @@ -31,44 +31,6 @@ class ServicesView extends ConsumerStatefulWidget { } class _ServicesViewState extends ConsumerState { - Future _showOpenBrowserWarning(BuildContext context, String url) async { - final uri = Uri.parse(url); - final shouldContinue = await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => StackDialog( - title: "Attention", - message: - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), - ), - rightButton: TextButton( - style: Theme.of( - context, - ).extension()!.getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text("Continue", style: STextStyles.button(context)), - ), - ), - ); - return shouldContinue ?? false; - } - void _showShopDialog() { showDialog( context: context, @@ -99,16 +61,11 @@ class _ServicesViewState extends ConsumerState { ..onTap = () async { const url = "https://api.shopinbit.com/static/policy/privacy.html"; - final shouldOpen = await _showOpenBrowserWarning( - dialogContext, - url, + + await showRequestExternalLinkAndMaybeLaunch( + context, + uri: Uri.parse(url), ); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } }, ), const TextSpan(text: "."), @@ -270,14 +227,11 @@ class _ServicesViewState extends ConsumerState { ..onTap = () async { const url = "https://api.shopinbit.com/static/policy/terms.html"; - final shouldOpen = - await _showOpenBrowserWarning(context, url); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } + + await showRequestExternalLinkAndMaybeLaunch( + context, + uri: Uri.parse(url), + ); }, ), const TextSpan(text: " and "), @@ -290,14 +244,11 @@ class _ServicesViewState extends ConsumerState { ..onTap = () async { const url = "https://api.shopinbit.com/static/policy/privacy.html"; - final shouldOpen = - await _showOpenBrowserWarning(context, url); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } + + await showRequestExternalLinkAndMaybeLaunch( + context, + uri: Uri.parse(url), + ); }, ), const TextSpan(text: "."), diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart index 72d95050d..d5d33475e 100644 --- a/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart +++ b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart @@ -1,13 +1,12 @@ import "package:flutter/gestures.dart"; import "package:flutter/material.dart"; -import "package:url_launcher/url_launcher.dart"; import "../../../utilities/text_styles.dart"; import "../../../utilities/util.dart"; import "../../../widgets/desktop/desktop_dialog.dart"; import "../../../widgets/desktop/primary_button.dart"; import "../../../widgets/desktop/secondary_button.dart"; -import "../../../widgets/stack_dialog.dart"; +import "../../../widgets/dialogs/request_external_link_navigation_dialog.dart"; const String _shopInBitPrivacyUrl = "https://api.shopinbit.com/static/policy/privacy.html"; @@ -22,45 +21,6 @@ class ShopInBitPrivacyCheckbox extends StatelessWidget { final bool value; final ValueChanged onChanged; - Future _openPrivacyPolicy(BuildContext context) async { - final bool shouldOpen = await _showOpenBrowserWarning( - context, - _shopInBitPrivacyUrl, - ); - if (shouldOpen) { - await launchUrl( - Uri.parse(_shopInBitPrivacyUrl), - mode: LaunchMode.externalApplication, - ); - } - } - - Future _showOpenBrowserWarning(BuildContext context, String url) async { - final Uri uri = Uri.parse(url); - final String message = - "You are about to open ${uri.scheme}://${uri.host} in your browser."; - - final bool? shouldContinue = await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Util.isDesktop - ? _DesktopBrowserWarning(message: message) - : StackDialog( - title: "Attention", - message: message, - leftButton: SecondaryButton( - label: "Cancel", - onPressed: () => Navigator.of(context).pop(false), - ), - rightButton: PrimaryButton( - label: "Continue", - onPressed: () => Navigator.of(context).pop(true), - ), - ), - ); - return shouldContinue ?? false; - } - @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; @@ -105,7 +65,10 @@ class ShopInBitPrivacyCheckbox extends StatelessWidget { context, ).copyWith(fontSize: isDesktop ? 18 : 14), recognizer: TapGestureRecognizer() - ..onTap = () => _openPrivacyPolicy(context), + ..onTap = () => showRequestExternalLinkAndMaybeLaunch( + context, + uri: Uri.parse(_shopInBitPrivacyUrl), + ), ), const TextSpan(text: "."), ], diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart index 85d1a19f0..302794ca3 100644 --- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart +++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../../app_config.dart'; import '../../../models/shopinbit/shopinbit_order_model.dart'; @@ -22,6 +21,7 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart'; +import '../../../widgets/dialogs/request_external_link_navigation_dialog.dart'; import '../../../widgets/rounded_container.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/textfields/adaptive_text_field.dart'; @@ -40,57 +40,6 @@ class DesktopShopInBitView extends ConsumerStatefulWidget { } class _DesktopServicesViewState extends ConsumerState { - Future _showOpenBrowserWarning(BuildContext context, String url) async { - final uri = Uri.parse(url); - final shouldContinue = await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => DesktopDialog( - maxWidth: 550, - maxHeight: 250, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), - child: Column( - children: [ - Text("Attention", style: STextStyles.desktopH2(context)), - const SizedBox(height: 16), - Text( - "You are about to open " - "${uri.scheme}://${uri.host} " - "in your browser.", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 35), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(false); - }, - ), - const SizedBox(width: 20), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(true); - }, - ), - ], - ), - ], - ), - ), - ), - ); - return shouldContinue ?? false; - } - Future _showShopDialog() async { final dao = ref.read(pSharedDrift).shopinBitSettingsDao; final settings = await dao.getSettings(); @@ -196,16 +145,10 @@ class _DesktopServicesViewState extends ConsumerState { ..onTap = () async { const url = "https://api.shopinbit.com/static/policy/terms.html"; - final shouldOpen = await _showOpenBrowserWarning( + await showRequestExternalLinkAndMaybeLaunch( context, - url, + uri: Uri.parse(url), ); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } }, ), const TextSpan(text: " and "), @@ -218,16 +161,11 @@ class _DesktopServicesViewState extends ConsumerState { ..onTap = () async { const url = "https://api.shopinbit.com/static/policy/privacy.html"; - final shouldOpen = await _showOpenBrowserWarning( + + await showRequestExternalLinkAndMaybeLaunch( context, - url, + uri: Uri.parse(url), ); - if (shouldOpen) { - await launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - } }, ), const TextSpan(text: "."), diff --git a/lib/widgets/dialogs/request_external_link_navigation_dialog.dart b/lib/widgets/dialogs/request_external_link_navigation_dialog.dart new file mode 100644 index 000000000..a5491a555 --- /dev/null +++ b/lib/widgets/dialogs/request_external_link_navigation_dialog.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../conditional_parent.dart'; +import '../desktop/desktop_dialog_close_button.dart'; +import '../desktop/primary_button.dart'; +import '../desktop/secondary_button.dart'; +import 's_dialog.dart'; + +Future showRequestExternalLinkAndMaybeLaunch( + BuildContext context, { + required Uri uri, +}) async { + final shouldContinue = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => RequestExternalLinkNavigationDialog(uri: uri), + ); + + if (shouldContinue == true) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } +} + +class RequestExternalLinkNavigationDialog extends StatefulWidget { + const RequestExternalLinkNavigationDialog({super.key, required this.uri}); + + final Uri uri; + + @override + State createState() => + _RequestExternalLinkNavigationDialogState(); +} + +class _RequestExternalLinkNavigationDialogState + extends State { + @override + Widget build(BuildContext context) { + return SDialog( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox(width: 500, child: child), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Padding( + padding: .only( + left: Util.isDesktop ? 32 : 16, + top: Util.isDesktop ? 0 : 16, + bottom: Util.isDesktop ? 16 : 8, + ), + child: Row( + mainAxisAlignment: .spaceBetween, + children: [ + SelectableText( + "Attention", + style: Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + ), + if (Util.isDesktop) const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: .symmetric(horizontal: Util.isDesktop ? 32 : 16), + child: Text( + "You are about to open " + "${widget.uri.scheme}://${widget.uri.host} " + "in your browser.", + style: Util.isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.smallMed14(context), + ), + ), + Padding( + padding: .only( + top: Util.isDesktop ? 32 : 24, + left: Util.isDesktop ? 32 : 16, + right: Util.isDesktop ? 32 : 16, + bottom: Util.isDesktop ? 32 : 16, + ), + child: Row( + mainAxisAlignment: .spaceBetween, + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? .l : null, + onPressed: Navigator.of(context).pop, + ), + ), + Util.isDesktop + ? const SizedBox(width: 32) + : const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Continue", + buttonHeight: Util.isDesktop ? .l : null, + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +}