diff --git a/lib/db/drift/shared_db/shared_database.dart b/lib/db/drift/shared_db/shared_database.dart new file mode 100644 index 0000000000..fa6f94e53e --- /dev/null +++ b/lib/db/drift/shared_db/shared_database.dart @@ -0,0 +1,95 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as path; + +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'; + +abstract final class SharedDrift { + static bool _didInit = false; + + static SharedDatabase? _db; + + static SharedDatabase get() { + if (!_didInit) { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + _didInit = true; + } + + return _db ??= SharedDatabase._(); + } +} + +@DriftDatabase( + tables: [CakepayOrders, ShopinBitSettings, ShopInBitTickets], + 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); + 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]) +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); + } +} 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 0000000000..24a3c83510 --- /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 0000000000..8dc7f82e62 --- /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 0000000000..e4c32532ed --- /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 0000000000..b8afcc969f --- /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 6c6a3e8ba5..3b86d74725 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 8206fc0f31..cf27091bf1 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 0a2ac53d7b..0000000000 --- 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 ecd600a154..0000000000 --- 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 f41aa49e39..c7d4e4df2c 100644 --- a/lib/models/shopinbit/shopinbit_order_model.dart +++ b/lib/models/shopinbit/shopinbit_order_model.dart @@ -1,10 +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 '../isar/models/shopinbit_ticket.dart'; +import '../../themes/stack_colors.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, @@ -16,7 +23,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 { @@ -230,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 @@ -287,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/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart index 089b492af1..7f7705b436 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, - ), + ), ), ], ), diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 7fbd0ebff9..07ec3d6039 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'; @@ -5,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'; @@ -15,9 +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 { @@ -34,33 +36,21 @@ 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(); - 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 { @@ -70,13 +60,13 @@ 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; 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; @@ -182,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( @@ -200,45 +190,41 @@ 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. - 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 { + 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, ); }, ); @@ -246,95 +232,356 @@ 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; 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, - ), - ), - selected: selected, - onSelected: (val) { - if (val) setState(() => _selectedDenomination = d); - }, - ); - }).toList(), - ) - : card.isRangeDenomination - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ConditionalParent( + condition: isDesktop, + builder: (child) => SDialog( + child: SizedBox( + width: 580, + child: Column( + mainAxisSize: .min, 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, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Gift Card", + style: STextStyles.desktopH3(context), + ), ), - 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, - ), - ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: child, ), ), ], - ) - : const SizedBox.shrink(); + ), + ), + ), + 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: SingleChildScrollView(child: child), + ), + ), + ), + ), + 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), + _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, + 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), + ], + ), + ), + ); + } +} + +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.onDenominationSelected, + required this.onCustomAmountChanged, + }); + + final CakePayCard card; + final bool isDesktop; + final Decimal? selectedDenomination; + final TextEditingController customAmountController; + 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(2)} ${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, + mainAxisSize: .min, + children: [ + Text( + "Enter amount (${card.minValue?.toStringAsFixed(2) ?? '?'} - " + "${card.maxValue?.toStringAsFixed(2) ?? '?'} " + "${card.currencyCode ?? ''})", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context), + ), + const SizedBox(height: 8), + AdaptiveTextField( + labelText: "Amount", + controller: customAmountController, + keyboardType: const .numberWithOptions(decimal: true), + onChangedComprehensive: (_) => onCustomAmountChanged(), + ), + ], + ); + } + + return const SizedBox.shrink(); + } +} + +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; - final quantityRow = Row( + @override + Widget build(BuildContext context) { + return Row( children: [ Text( "Quantity", @@ -345,23 +592,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 +637,7 @@ class _CakePayCardDetailViewState extends State { child: IgnorePointer( child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _termsAccepted, + value: accepted, onChanged: (_) {}, ), ), @@ -392,7 +656,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,231 +674,5 @@ 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, - ), - ], - ), - ); - - return _scaffold(isDesktop: isDesktop, child: content); - } - - 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, - ), - ), - ], - ), - ), - 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.all(16), child: child), - ), - ), - ), - child: child, - ), - ); } } diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart index 71c9fe89a9..62fc0bd41d 100644 --- a/lib/pages/cakepay/cakepay_order_view.dart +++ b/lib/pages/cakepay/cakepay_order_view.dart @@ -20,9 +20,10 @@ 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'; 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) @@ -937,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( diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index e1fd13513e..48b966507e 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'; @@ -38,18 +39,13 @@ 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) { 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/cakepay/cakepay_vendors_view.dart b/lib/pages/cakepay/cakepay_vendors_view.dart index 2c7b3f5cbb..5c16cdd7dd 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/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 1a3a88db9b..700088664c 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/more_view/gift_cards_view.dart b/lib/pages/more_view/gift_cards_view.dart index 9fcf82bbca..48ff0f3646 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/more_view/services_view.dart b/lib/pages/more_view/services_view.dart index aa4d7acdaa..b240d60f33 100644 --- a/lib/pages/more_view/services_view.dart +++ b/lib/pages/more_view/services_view.dart @@ -1,10 +1,12 @@ +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'; 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,14 +308,17 @@ class _ServicesViewState extends State { PrimaryButton( label: "Shop with ShopinBit", enabled: true, - onPressed: () => _showShopDialog(context), + 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 ca102c4c59..5a4a3bb704 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -14,11 +14,8 @@ 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 '../../../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'; @@ -313,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( @@ -369,25 +334,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 +374,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_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart index 7f691375d2..e822e87714 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/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'; 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,13 +269,17 @@ 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; 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); @@ -328,7 +337,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 +482,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 0073ee831e..8ef075b706 100644 --- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart @@ -6,13 +6,12 @@ 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'; +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 +21,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 +239,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 +345,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 +396,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 +429,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); @@ -475,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); @@ -517,7 +530,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,26 +550,33 @@ 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()); + final db = ref.read(pSharedDrift); + await db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(widget.model.toCompanion()); 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 @@ -596,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; @@ -645,13 +671,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) { @@ -673,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 de7bc1770e..a6f85da132 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_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart index ace2f3d37d..98946c14dd 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'; @@ -11,10 +12,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/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"; @@ -22,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 @@ -39,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( @@ -154,14 +157,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 +182,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 +220,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 0467d3fb7e..eea1f437c8 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'; @@ -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'; @@ -107,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) { @@ -122,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!); } @@ -141,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!); } @@ -159,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; @@ -471,14 +475,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 +733,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { child: Stack( children: [ SingleChildScrollView(child: content), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ), ), @@ -779,7 +775,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { ), ), ), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ); }, diff --git a/lib/pages/shopinbit/shopinbit_send_from_view.dart b/lib/pages/shopinbit/shopinbit_send_from_view.dart index 0060cf596f..d2c08e26b3 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 0682b74e6e..51fb674d50 100644 --- a/lib/pages/shopinbit/shopinbit_settings_view.dart +++ b/lib/pages/shopinbit/shopinbit_settings_view.dart @@ -6,18 +6,24 @@ 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/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'; -import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfields/adaptive_text_field.dart'; class ShopInBitSettingsView extends ConsumerStatefulWidget { const ShopInBitSettingsView({super.key}); @@ -31,11 +37,7 @@ 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 +46,28 @@ 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(); @@ -163,383 +173,691 @@ class _ShopInBitSettingsViewState extends ConsumerState { } Future _showChangeWarning() async { - final result = await showDialog( + final confirmSaved = 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(), + ], ), - 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), - ), + 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(), + ), + ), + const SizedBox(width: 24), + Expanded( + child: PrimaryButton( + label: "I saved my key", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], ), ), ], ), - ], - ), - ), - ); - - 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 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, - ), - child: TextField( - controller: _verifyKeyController, - focusNode: _verifyKeyFocusNode, - style: STextStyles.field(ctx), - decoration: standardInputDecoration( - "Enter current key", - _verifyKeyFocusNode, - ctx, - ), - onChanged: (_) => setDialogState(() {}), + ); + } 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: 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(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(), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of( + context, + ).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: 16), + Expanded( + child: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () => Navigator.of(context).pop(true), + child: Text( + "I saved my key", + style: STextStyles.button(context), ), ), - ], - ), - ], - ), - ); - }, - ); + ), + ], + ), + ], + ), + ); + } }, ); + + if (confirmSaved != true || !mounted) return false; + + return showDialog( + context: context, + barrierDismissible: true, + builder: (_) => _VerifyKeyDialog(currentKey: _currentKey!), + ); } @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 (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, + ), ), - ), - ], + ], + ), ), + ] 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), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), + ), + ), + ); + } + } +} + +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), + ), + ), + Util.isDesktop + ? const SizedBox(width: 24) + : 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, + ), + ), + ], + ), + ], + ), + ), ); } } diff --git a/lib/pages/shopinbit/shopinbit_setup_view.dart b/lib/pages/shopinbit/shopinbit_setup_view.dart index 5566d5320c..1ce525f258 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 013b276da2..03ae923542 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_1.dart b/lib/pages/shopinbit/shopinbit_step_1.dart index 6e6a097c42..a1fa23694c 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,173 +27,140 @@ class ShopInBitStep1 extends StatefulWidget { class _ShopInBitStep1State extends State { late final TextEditingController _nameController; - late final FocusNode _nameFocusNode; - bool get _canContinue => _nameController.text.trim().isNotEmpty; + bool _canContinue = false; + + void _continue() { + widget.model.displayName = _nameController.text.trim(); + Navigator.of( + context, + ).pushNamed(ShopInBitStep2.routeName, arguments: widget.model); + } @override void initState() { super.initState(); + _canContinue = widget.model.displayName.isNotEmpty; _nameController = TextEditingController(text: widget.model.displayName); - _nameFocusNode = FocusNode(); - - _nameFocusNode.addListener(() { - setState(() {}); - }); } @override void dispose() { _nameController.dispose(); - _nameFocusNode.dispose(); super.dispose(); } - 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); - } - } - @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 9df909ef52..23403ce600 100644 --- a/lib/pages/shopinbit/shopinbit_step_2.dart +++ b/lib/pages/shopinbit/shopinbit_step_2.dart @@ -1,23 +1,25 @@ 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'; 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'; 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"; @@ -25,12 +27,31 @@ class ShopInBitStep2 extends StatefulWidget { final ShopInBitOrderModel model; @override - State createState() => _ShopInBitStep2State(); + ConsumerState createState() => _ShopInBitStep2State(); } -class _ShopInBitStep2State extends State { +class _ShopInBitStep2State extends ConsumerState { ShopInBitCategory? _selected; + Future _continue() async { + widget.model.category = _selected; + final skipGuidelines = + (await ref.read(pSharedDrift).shopinBitSettingsDao.getSettings()) + .guidelinesAccepted; + if (!mounted) return; + + if (skipGuidelines) { + widget.model.guidelinesAccepted = true; + await Navigator.of( + context, + ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); + } else { + await Navigator.of( + context, + ).pushNamed(ShopInBitStep3.routeName, arguments: widget.model); + } + } + @override void initState() { super.initState(); @@ -39,257 +60,209 @@ class _ShopInBitStep2State extends State { _selected = null; } - 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); - } - } 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); - } - } - } + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; - 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, - ), - 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, - ), - ), - SizedBox(width: isDesktop ? 16 : 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ConditionalParent( + condition: isDesktop, + builder: (content) => SDialog( + child: SizedBox( + width: 580, + child: Column( + mainAxisSize: .min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, 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, - ), + Row( + children: [ + const AppBarBackButton(isCompact: true, iconSize: 23), + Text("ShopinBit", style: STextStyles.desktopH3(context)), + ], ), + const DesktopDialogCloseButton(), ], ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: content, + ), + ), + ], + ), + ), + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (content) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text("ShopinBit", style: STextStyles.navBarTitle(context)), ), - if (isSelected) - Icon( - Icons.check_circle, - color: Theme.of(context).extension()!.textDark, - size: isDesktop ? 24 : 20, + 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: 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/shopinbit/shopinbit_step_3.dart b/lib/pages/shopinbit/shopinbit_step_3.dart index 21f7b146f7..f84d487c2f 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'; @@ -12,10 +13,9 @@ 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 { +class ShopInBitStep3 extends ConsumerStatefulWidget { const ShopInBitStep3({super.key, required this.model}); static const String routeName = "/shopInBitStep3"; @@ -23,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() { @@ -74,35 +74,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); - } + ref.read(pSharedDrift).shopinBitSettingsDao.setGuidelinesAccepted(true); + + Navigator.of( + context, + ).pushNamed(ShopInBitStep4.routeName, arguments: widget.model); } @override @@ -184,11 +163,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 +188,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 3ead68b6e8..c5cfd4fff8 100644 --- a/lib/pages/shopinbit/shopinbit_step_4.dart +++ b/lib/pages/shopinbit/shopinbit_step_4.dart @@ -1,37 +1,20 @@ -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 'dart:async'; - -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/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_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'; - -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"; @@ -39,2431 +22,89 @@ 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: (_) => 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; - } - - 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(); - } - - 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 { - 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), - ), - ), - ], - ), - ), - ), - 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, - ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: SingleChildScrollView(child: content), ), ), - 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: [ - AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: _popBack, - ), - 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: 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_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 85ceb97cf0..7f863dd18d 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/db/drift_provider.dart'; +import '../../providers/global/shopin_bit_service_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; @@ -15,10 +16,11 @@ 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'; -class ShopInBitTicketDetail extends StatefulWidget { +class ShopInBitTicketDetail extends ConsumerStatefulWidget { const ShopInBitTicketDetail({super.key, required this.model}); static const String routeName = "/shopInBitTicketDetail"; @@ -26,57 +28,13 @@ 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; - 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; @@ -109,52 +67,50 @@ 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; - // 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, + ); } } + 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 @@ -178,15 +134,18 @@ 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(); } + 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 @@ -201,18 +160,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) { @@ -237,10 +199,15 @@ class _ShopInBitTicketDetailState extends State { ..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); @@ -424,15 +391,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()!, + ), + ), ), ), ], @@ -497,14 +470,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 ce62d3be35..76cfc8757a 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -1,68 +1,68 @@ -import 'dart:async'; -import 'dart:convert'; +import "dart:async"; +import "dart:convert"; -import 'package:flutter/material.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 '../../services/shopinbit/shopinbit_service.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/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 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; - 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; @@ -111,20 +111,22 @@ 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); 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; + // 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( @@ -134,7 +136,7 @@ class _ShopInBitTicketsViewState extends State { 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, @@ -143,7 +145,7 @@ class _ShopInBitTicketsViewState extends State { } } - 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!) { @@ -157,77 +159,26 @@ class _ShopInBitTicketsViewState extends State { } } - 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 _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: - 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) { @@ -358,13 +309,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( @@ -374,9 +328,10 @@ class _ShopInBitTicketsViewState extends State { context, )) .copyWith( - color: _statusColor( - context, - ticket.status, + color: ticket.status.getColor( + Theme.of( + context, + ).extension()!, ), ), ), @@ -446,14 +401,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/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 0000000000..9a8ff9ded1 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_car_research_form.dart @@ -0,0 +1,349 @@ +import "dart:async"; + +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/db/drift_provider.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 ConsumerStatefulWidget { + const ShopInBitCarResearchForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + ConsumerState createState() => + _ShopInBitCarResearchFormState(); +} + +class _ShopInBitCarResearchFormState + extends ConsumerState { + 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 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( + 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 0000000000..480f427272 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart @@ -0,0 +1,201 @@ +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/db/drift_provider.dart"; +import "../../../providers/global/shopin_bit_service_provider.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 ConsumerStatefulWidget { + const ShopInBitConciergeForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + ConsumerState createState() => + _ShopInBitConciergeFormState(); +} + +class _ShopInBitConciergeFormState + extends ConsumerState { + 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, + ref.read(pShopinBitService), + ref.read(pSharedDrift), + ); + } 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 0000000000..f0feb9db67 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart @@ -0,0 +1,167 @@ +import "package:dropdown_button2/dropdown_button2.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_svg/svg.dart"; + +import "../../../providers/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 ConsumerStatefulWidget { + const ShopInBitCountryPicker({ + super.key, + required this.selectedIso, + required this.onChanged, + this.hintText = "Delivery country", + }); + + final String? selectedIso; + final ValueChanged onChanged; + final String hintText; + + @override + ConsumerState createState() => + _ShopInBitCountryPickerState(); +} + +class _ShopInBitCountryPickerState + extends ConsumerState { + 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 ref.read(pShopinBitService).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 0000000000..9fcb5b958e --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart @@ -0,0 +1,121 @@ +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 "../../../providers/providers.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 ConsumerStatefulWidget { + const ShopInBitGenericForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + ConsumerState createState() => + _ShopInBitGenericFormState(); +} + +class _ShopInBitGenericFormState extends ConsumerState { + 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, + ref.read(pShopinBitService), + ref.read(pSharedDrift), + ); + } 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 0000000000..6f4014f88b --- /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 0000000000..72d95050d5 --- /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 0000000000..baae092879 --- /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 0000000000..4c81d7df15 --- /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 0000000000..567a5c52b0 --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart @@ -0,0 +1,99 @@ +import "dart:async"; + +import "package:flutter/material.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"; +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, + ShopInBitService service, + SharedDatabase db, +) async { + try { + 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 db + .into(db.shopInBitTickets) + .insertOnConflictUpdate(model.toCompanion()); + + 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 0000000000..ac38c46bb9 --- /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 0000000000..7cdc97a30c --- /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 0000000000..a1e505f33e --- /dev/null +++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart @@ -0,0 +1,553 @@ +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/db/drift_provider.dart"; +import "../../../providers/global/shopin_bit_service_provider.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 ConsumerStatefulWidget { + const ShopInBitTravelForm({super.key, required this.model}); + + final ShopInBitOrderModel model; + + @override + ConsumerState createState() => + _ShopInBitTravelFormState(); +} + +class _ShopInBitTravelFormState extends ConsumerState { + 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, + ref.read(pShopinBitService), + ref.read(pSharedDrift), + ); + } 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 0000000000..fb5ab6d412 --- /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)), + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 12affd42b9..83a4d6e8fa 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/cakepay/desktop_gift_cards_view.dart similarity index 90% 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 index 7693f43572..964028acb8 100644 --- 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 @@ -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/pages_desktop_specific/services/desktop_services_view.dart b/lib/pages_desktop_specific/services/desktop_services_view.dart index 26b6e9e59f..f94f708831 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 78% 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 e5c9e596a2..c58abbc3af 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 @@ -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,14 +7,13 @@ 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'; -import '../../../pages/shopinbit/shopinbit_step_2.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'; @@ -21,11 +21,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}); @@ -89,12 +91,16 @@ class _DesktopServicesViewState extends ConsumerState { return shouldContinue ?? false; } - void _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, @@ -105,7 +111,7 @@ 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; } @@ -116,93 +122,12 @@ class _DesktopServicesViewState extends ConsumerState { 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( - 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.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, - 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: (_) => ShopInBitStep1(model: model), - ); - if (mounted) setState(() {}); - }, - ), - ], - ), - ], - ), - ), + builder: (_) => NestedNavigatorDialog( + initialRoute: DesktopShopinBitFirstRun.routeName, + initialRouteArgs: model, ), ); } else { @@ -210,8 +135,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(() {}); } } @@ -314,14 +244,18 @@ class _DesktopServicesViewState extends ConsumerState { buttonHeight: ButtonHeight.m, enabled: true, label: "Shop with ShopinBit", - onPressed: () => _showShopDialog(context), + 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, @@ -371,29 +305,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 @@ -405,8 +351,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 new file mode 100644 index 0000000000..b693f5d3fd --- /dev/null +++ b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart @@ -0,0 +1,69 @@ +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: 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, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart index 4247186964..ee2c423b3d 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 146243c96e..0000000000 --- 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 658dd5bc7e..efbf436498 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_db/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 0000000000..9f9c422e69 --- /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 0f0fbbf58e..5aa23961d8 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'; @@ -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/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 1016bc4b77..fced753afc 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,51 +1,42 @@ -import '../../db/hive/db.dart'; +import 'package:drift/drift.dart'; + +import '../../db/drift/shared_db/shared_database.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 { 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(); } } diff --git a/lib/services/cakepay/src/models/card.dart b/lib/services/cakepay/src/models/card.dart index 2fed2f47e0..83d2eb3bc1 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/services/shopinbit/shopinbit_service.dart b/lib/services/shopinbit/shopinbit_service.dart index b0669433a4..d8d2cee319 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/lib/services/shopinbit/src/models/ticket.dart b/lib/services/shopinbit/src/models/ticket.dart index eec6dd3604..06093ff21c 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"), ); } } @@ -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,11 +126,26 @@ 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 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 7bf41694e8..67a160b2cf 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"), ); } } 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 0000000000..ae54f23f28 --- /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 0000000000..7d924861bc --- /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", + ), + ), + ), + ); + } +} diff --git a/lib/widgets/dialogs/s_dialog.dart b/lib/widgets/dialogs/s_dialog.dart index a6b32148c4..6bf66ecf4a 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/icon_widgets/credit_card_icon.dart b/lib/widgets/icon_widgets/credit_card_icon.dart new file mode 100644 index 0000000000..369792e562 --- /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, + ), + ); + } +} diff --git a/lib/widgets/textfields/adaptive_text_field.dart b/lib/widgets/textfields/adaptive_text_field.dart index e57746a80f..da30057e8b 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, diff --git a/test/services/paynym/paynym_is_api_test.mocks.dart b/test/services/paynym/paynym_is_api_test.mocks.dart index e3d6837fa8..c62d8cc0c1 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>); } diff --git a/test/shopinbit/car_research_persistence_test.dart b/test/shopinbit/car_research_persistence_test.dart index 10df9aee52..b68fd5b64b 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 20f8278524..3db3e7075e 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].