refactor(shopinbit): Store shop in bit settings using Drift, use providers for drift shared db and shopinbit service, and some general clean up and tweaks
What changed, and why it matters
This commit is a routine refactor of the ShopinBit feature in Stack Wallet. It moves settings like whether the user accepted guidelines, completed setup, and their display name from the previous storage mechanism into a Drift/SQLite database, and switches UI code to use Riverpod providers for that database and the ShopInBit service. The diff shows no obvious malicious intent, no new network calls, no weakening of encryption, and no exposed secrets. It is primarily a code-quality and architecture change.
No immediate security action required. Treat as a normal refactor. If reviewing further, verify that the Drift provider correctly scopes the shared database lifecycle and that the removed `ShopInBitService.instance` persistence methods are no longer used elsewhere. Also confirm the migration handles downgrade/failure gracefully and that the generated `.g.dart` file matches the source table definition.
Security signals we found
Refactor only: storage backend changed, no new security-sensitive operations introduced
Customer key generation/loading still handled by ShopInBitService, not altered in this diff
No SQL injection indicators: Drift generated code uses parameterized queries
No hardcoded secrets, keys, or credentials added
No new network endpoints or permissions introduced
Migration is additive (creates one table) and does not drop existing data
Evidence from the diff
The patch adds a new ShopinBitSettings Drift table (single-row, id=0) with columns guidelinesAccepted, setupComplete, and nullable displayName, plus a DAO and a schema migration from version 1 to 2. It then updates many ShopinBit UI pages to consume pSharedDrift and pShopinBitService Riverpod providers instead of directly accessing ShopInBitService.instance. The settings read/write methods on the service are replaced by DAO calls. There are also UI cleanups (desktop/mobile conditional layouts, AdaptiveTextField, ColorFilter fixes). No security-critical logic such as key generation, encryption, or authentication appears to be changed in substance.
Changed components
lib/db/drift/shared_database.dartlib/db/drift/shared_database.g.dartlib/providers/db/drift_provider.dartlib/providers/global/shopin_bit_service_provider.dartlib/services/shopinbit/shopinbit_service.dartlib/pages/shopinbit/*lib/pages_desktop_specific/services/shopin_bit/*Inspect captured patch +1708 / −1266
diff --git a/lib/db/drift/shared_database.dart b/lib/db/drift/shared_database.dart
index 2e00d3c..e83028d 100644
--- a/lib/db/drift/shared_database.dart
+++ b/lib/db/drift/shared_database.dart
@@ -28,13 +28,72 @@ class CakepayOrders extends Table {
Set<Column> get primaryKey => {orderId};
}
-@DriftDatabase(tables: [CakepayOrders])
+class ShopinBitSettings extends Table {
+ // Single row table - always row 0
+ IntColumn get id => integer().withDefault(const Constant(0))();
+
+ BoolColumn get guidelinesAccepted =>
+ boolean().withDefault(const Constant(false))();
+ BoolColumn get setupComplete =>
+ boolean().withDefault(const Constant(false))();
+ TextColumn get displayName => text().nullable()();
+
+ @override
+ Set<Column> get primaryKey => {id};
+}
+
+@DriftAccessor(tables: [ShopinBitSettings])
+class ShopinBitSettingsDao extends DatabaseAccessor<SharedDatabase>
+ with _$ShopinBitSettingsDaoMixin {
+ ShopinBitSettingsDao(super.db);
+
+ Future<ShopinBitSetting> 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<void> setGuidelinesAccepted(bool accepted) =>
+ _update(ShopinBitSettingsCompanion(guidelinesAccepted: Value(accepted)));
+
+ Future<void> setSetupComplete(bool complete) =>
+ _update(ShopinBitSettingsCompanion(setupComplete: Value(complete)));
+
+ Future<void> setDisplayName(String name) =>
+ _update(ShopinBitSettingsCompanion(displayName: Value(name)));
+
+ Future<void> _update(ShopinBitSettingsCompanion changes) async {
+ await getSettings(); // ensure row exists
+ await (update(
+ shopinBitSettings,
+ )..where((t) => t.id.equals(0))).write(changes);
+ }
+}
+
+@DriftDatabase(
+ tables: [CakepayOrders, ShopinBitSettings],
+ daos: [ShopinBitSettingsDao],
+)
final class SharedDatabase extends _$SharedDatabase {
SharedDatabase._([QueryExecutor? executor])
: super(executor ?? _openConnection());
@override
- int get schemaVersion => 1;
+ int get schemaVersion => 2;
+
+ @override
+ MigrationStrategy get migration => MigrationStrategy(
+ onUpgrade: (m, from, to) async {
+ if (from == 1 && to == 2) {
+ await m.createTable(shopinBitSettings);
+ }
+ },
+ );
static QueryExecutor _openConnection() {
return driftDatabase(
diff --git a/lib/db/drift/shared_database.g.dart b/lib/db/drift/shared_database.g.dart
index 2e0c5de..9b7d8d7 100644
--- a/lib/db/drift/shared_database.g.dart
+++ b/lib/db/drift/shared_database.g.dart
@@ -3,6 +3,22 @@
part of 'shared_database.dart';
// ignore_for_file: type=lint
+mixin _$ShopinBitSettingsDaoMixin on DatabaseAccessor<SharedDatabase> {
+ $ShopinBitSettingsTable get shopinBitSettings =>
+ attachedDatabase.shopinBitSettings;
+ ShopinBitSettingsDaoManager get managers => ShopinBitSettingsDaoManager(this);
+}
+
+class ShopinBitSettingsDaoManager {
+ final _$ShopinBitSettingsDaoMixin _db;
+ ShopinBitSettingsDaoManager(this._db);
+ $$ShopinBitSettingsTableTableManager get shopinBitSettings =>
+ $$ShopinBitSettingsTableTableManager(
+ _db.attachedDatabase,
+ _db.shopinBitSettings,
+ );
+}
+
class $CakepayOrdersTable extends CakepayOrders
with TableInfo<$CakepayOrdersTable, CakepayOrder> {
@override
@@ -165,15 +181,345 @@ class CakepayOrdersCompanion extends UpdateCompanion<CakepayOrder> {
}
}
+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<int> id = GeneratedColumn<int>(
+ 'id',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultValue: const Constant(0),
+ );
+ static const VerificationMeta _guidelinesAcceptedMeta =
+ const VerificationMeta('guidelinesAccepted');
+ @override
+ late final GeneratedColumn<bool> guidelinesAccepted = GeneratedColumn<bool>(
+ '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<bool> setupComplete = GeneratedColumn<bool>(
+ '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<String> displayName = GeneratedColumn<String>(
+ 'display_name',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ @override
+ List<GeneratedColumn> 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<ShopinBitSetting> 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<GeneratedColumn> get $primaryKey => {id};
+ @override
+ ShopinBitSetting map(Map<String, dynamic> 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<ShopinBitSetting> {
+ 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<String, Expression> toColumns(bool nullToAbsent) {
+ final map = <String, Expression>{};
+ map['id'] = Variable<int>(id);
+ map['guidelines_accepted'] = Variable<bool>(guidelinesAccepted);
+ map['setup_complete'] = Variable<bool>(setupComplete);
+ if (!nullToAbsent || displayName != null) {
+ map['display_name'] = Variable<String>(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<String, dynamic> json, {
+ ValueSerializer? serializer,
+ }) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return ShopinBitSetting(
+ id: serializer.fromJson<int>(json['id']),
+ guidelinesAccepted: serializer.fromJson<bool>(json['guidelinesAccepted']),
+ setupComplete: serializer.fromJson<bool>(json['setupComplete']),
+ displayName: serializer.fromJson<String?>(json['displayName']),
+ );
+ }
+ @override
+ Map<String, dynamic> toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return <String, dynamic>{
+ 'id': serializer.toJson<int>(id),
+ 'guidelinesAccepted': serializer.toJson<bool>(guidelinesAccepted),
+ 'setupComplete': serializer.toJson<bool>(setupComplete),
+ 'displayName': serializer.toJson<String?>(displayName),
+ };
+ }
+
+ ShopinBitSetting copyWith({
+ int? id,
+ bool? guidelinesAccepted,
+ bool? setupComplete,
+ Value<String?> 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<ShopinBitSetting> {
+ final Value<int> id;
+ final Value<bool> guidelinesAccepted;
+ final Value<bool> setupComplete;
+ final Value<String?> 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<ShopinBitSetting> custom({
+ Expression<int>? id,
+ Expression<bool>? guidelinesAccepted,
+ Expression<bool>? setupComplete,
+ Expression<String>? 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<int>? id,
+ Value<bool>? guidelinesAccepted,
+ Value<bool>? setupComplete,
+ Value<String?>? displayName,
+ }) {
+ return ShopinBitSettingsCompanion(
+ id: id ?? this.id,
+ guidelinesAccepted: guidelinesAccepted ?? this.guidelinesAccepted,
+ setupComplete: setupComplete ?? this.setupComplete,
+ displayName: displayName ?? this.displayName,
+ );
+ }
+
+ @override
+ Map<String, Expression> toColumns(bool nullToAbsent) {
+ final map = <String, Expression>{};
+ if (id.present) {
+ map['id'] = Variable<int>(id.value);
+ }
+ if (guidelinesAccepted.present) {
+ map['guidelines_accepted'] = Variable<bool>(guidelinesAccepted.value);
+ }
+ if (setupComplete.present) {
+ map['setup_complete'] = Variable<bool>(setupComplete.value);
+ }
+ if (displayName.present) {
+ map['display_name'] = Variable<String>(displayName.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('ShopinBitSettingsCompanion(')
+ ..write('id: $id, ')
+ ..write('guidelinesAccepted: $guidelinesAccepted, ')
+ ..write('setupComplete: $setupComplete, ')
+ ..write('displayName: $displayName')
+ ..write(')'))
+ .toString();
+ }
+}
+
abstract class _$SharedDatabase extends GeneratedDatabase {
_$SharedDatabase(QueryExecutor e) : super(e);
$SharedDatabaseManager get managers => $SharedDatabaseManager(this);
late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this);
+ late final $ShopinBitSettingsTable shopinBitSettings =
+ $ShopinBitSettingsTable(this);
+ late final ShopinBitSettingsDao shopinBitSettingsDao = ShopinBitSettingsDao(
+ this as SharedDatabase,
+ );
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
- List<DatabaseSchemaEntity> get allSchemaEntities => [cakepayOrders];
+ List<DatabaseSchemaEntity> get allSchemaEntities => [
+ cakepayOrders,
+ shopinBitSettings,
+ ];
}
typedef $$CakepayOrdersTableCreateCompanionBuilder =
@@ -294,10 +640,206 @@ typedef $$CakepayOrdersTableProcessedTableManager =
CakepayOrder,
PrefetchHooks Function()
>;
+typedef $$ShopinBitSettingsTableCreateCompanionBuilder =
+ ShopinBitSettingsCompanion Function({
+ Value<int> id,
+ Value<bool> guidelinesAccepted,
+ Value<bool> setupComplete,
+ Value<String?> displayName,
+ });
+typedef $$ShopinBitSettingsTableUpdateCompanionBuilder =
+ ShopinBitSettingsCompanion Function({
+ Value<int> id,
+ Value<bool> guidelinesAccepted,
+ Value<bool> setupComplete,
+ Value<String?> displayName,
+ });
+
+class $$ShopinBitSettingsTableFilterComposer
+ extends Composer<_$SharedDatabase, $ShopinBitSettingsTable> {
+ $$ShopinBitSettingsTableFilterComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ ColumnFilters<int> get id => $composableBuilder(
+ column: $table.id,
+ builder: (column) => ColumnFilters(column),
+ );
+
+ ColumnFilters<bool> get guidelinesAccepted => $composableBuilder(
+ column: $table.guidelinesAccepted,
+ builder: (column) => ColumnFilters(column),
+ );
+
+ ColumnFilters<bool> get setupComplete => $composableBuilder(
+ column: $table.setupComplete,
+ builder: (column) => ColumnFilters(column),
+ );
+
+ ColumnFilters<String> 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<int> get id => $composableBuilder(
+ column: $table.id,
+ builder: (column) => ColumnOrderings(column),
+ );
+
+ ColumnOrderings<bool> get guidelinesAccepted => $composableBuilder(
+ column: $table.guidelinesAccepted,
+ builder: (column) => ColumnOrderings(column),
+ );
+
+ ColumnOrderings<bool> get setupComplete => $composableBuilder(
+ column: $table.setupComplete,
+ builder: (column) => ColumnOrderings(column),
+ );
+
+ ColumnOrderings<String> 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<int> get id =>
+ $composableBuilder(column: $table.id, builder: (column) => column);
+
+ GeneratedColumn<bool> get guidelinesAccepted => $composableBuilder(
+ column: $table.guidelinesAccepted,
+ builder: (column) => column,
+ );
+
+ GeneratedColumn<bool> get setupComplete => $composableBuilder(
+ column: $table.setupComplete,
+ builder: (column) => column,
+ );
+
+ GeneratedColumn<String> 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<int> id = const Value.absent(),
+ Value<bool> guidelinesAccepted = const Value.absent(),
+ Value<bool> setupComplete = const Value.absent(),
+ Value<String?> displayName = const Value.absent(),
+ }) => ShopinBitSettingsCompanion(
+ id: id,
+ guidelinesAccepted: guidelinesAccepted,
+ setupComplete: setupComplete,
+ displayName: displayName,
+ ),
+ createCompanionCallback:
+ ({
+ Value<int> id = const Value.absent(),
+ Value<bool> guidelinesAccepted = const Value.absent(),
+ Value<bool> setupComplete = const Value.absent(),
+ Value<String?> displayName = const Value.absent(),
+ }) => ShopinBitSettingsCompanion.insert(
+ id: id,
+ guidelinesAccepted: guidelinesAccepted,
+ setupComplete: setupComplete,
+ displayName: displayName,
+ ),
+ withReferenceMapper: (p0) => p0
+ .map((e) => (e.readTable(table), BaseReferences(db, table, e)))
+ .toList(),
+ prefetchHooksCallback: null,
+ ),
+ );
+}
+
+typedef $$ShopinBitSettingsTableProcessedTableManager =
+ ProcessedTableManager<
+ _$SharedDatabase,
+ $ShopinBitSettingsTable,
+ ShopinBitSetting,
+ $$ShopinBitSettingsTableFilterComposer,
+ $$ShopinBitSettingsTableOrderingComposer,
+ $$ShopinBitSettingsTableAnnotationComposer,
+ $$ShopinBitSettingsTableCreateCompanionBuilder,
+ $$ShopinBitSettingsTableUpdateCompanionBuilder,
+ (
+ ShopinBitSetting,
+ BaseReferences<
+ _$SharedDatabase,
+ $ShopinBitSettingsTable,
+ ShopinBitSetting
+ >,
+ ),
+ ShopinBitSetting,
+ PrefetchHooks Function()
+ >;
class $SharedDatabaseManager {
final _$SharedDatabase _db;
$SharedDatabaseManager(this._db);
$$CakepayOrdersTableTableManager get cakepayOrders =>
$$CakepayOrdersTableTableManager(_db, _db.cakepayOrders);
+ $$ShopinBitSettingsTableTableManager get shopinBitSettings =>
+ $$ShopinBitSettingsTableTableManager(_db, _db.shopinBitSettings);
}
diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart
index aa4d7ac..210c454 100644
--- a/lib/pages/more_view/services_view.dart
+++ b/lib/pages/more_view/services_view.dart
@@ -1,10 +1,12 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../db/isar/main_db.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../providers/providers.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
@@ -14,23 +16,21 @@ import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
import '../shopinbit/shopinbit_settings_view.dart';
import '../shopinbit/shopinbit_setup_view.dart';
-import '../shopinbit/shopinbit_step_1.dart';
import '../shopinbit/shopinbit_step_2.dart';
import '../shopinbit/shopinbit_tickets_view.dart';
-class ServicesView extends StatefulWidget {
+class ServicesView extends ConsumerStatefulWidget {
const ServicesView({super.key});
static const String routeName = "/servicesView";
@override
- State<ServicesView> createState() => _ServicesViewState();
+ ConsumerState<ServicesView> createState() => _ServicesViewState();
}
-class _ServicesViewState extends State<ServicesView> {
+class _ServicesViewState extends ConsumerState<ServicesView> {
Future<bool> _showOpenBrowserWarning(BuildContext context, String url) async {
final uri = Uri.parse(url);
final shouldContinue = await showDialog<bool>(
@@ -69,7 +69,7 @@ class _ServicesViewState extends State<ServicesView> {
return shouldContinue ?? false;
}
- void _showShopDialog(BuildContext context) {
+ void _showShopDialog() {
showDialog<void>(
context: context,
barrierDismissible: true,
@@ -142,12 +142,17 @@ class _ServicesViewState extends State<ServicesView> {
onPressed: () async {
Navigator.of(dialogContext).pop();
final model = ShopInBitOrderModel();
- final service = ShopInBitService.instance;
+ final settings = await ref
+ .read(pSharedDrift)
+ .shopinBitSettingsDao
+ .getSettings();
- if (service.loadSetupComplete()) {
+ if (!mounted) return;
+
+ if (settings.setupComplete) {
// Returning user: pre-load display name,
// skip Step 1, go to Step 2
- final savedName = service.loadDisplayName();
+ final savedName = settings.displayName;
if (savedName != null && savedName.isNotEmpty) {
model.displayName = savedName;
}
@@ -303,7 +308,7 @@ class _ServicesViewState extends State<ServicesView> {
PrimaryButton(
label: "Shop with ShopinBit",
enabled: true,
- onPressed: () => _showShopDialog(context),
+ onPressed: _showShopDialog,
),
const SizedBox(height: 12),
Builder(
diff --git a/lib/pages/shopinbit/shopinbit_car_fee_view.dart b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
index 7f69137..4f88893 100644
--- a/lib/pages/shopinbit/shopinbit_car_fee_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_fee_view.dart
@@ -3,12 +3,13 @@ import 'dart:convert';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../db/isar/main_db.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../services/shopinbit/src/models/address.dart';
import '../../services/shopinbit/src/models/car_research.dart';
import '../../themes/stack_colors.dart';
@@ -17,7 +18,6 @@ import '../../utilities/constants.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
-import '../more_view/services_view.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
@@ -25,10 +25,11 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_text_field.dart';
+import '../more_view/services_view.dart';
import 'shopinbit_car_research_payment_view.dart';
import 'shopinbit_step_2.dart';
-class ShopInBitCarFeeView extends StatefulWidget {
+class ShopInBitCarFeeView extends ConsumerStatefulWidget {
const ShopInBitCarFeeView({super.key, required this.model});
static const String routeName = "/shopInBitCarFee";
@@ -36,10 +37,11 @@ class ShopInBitCarFeeView extends StatefulWidget {
final ShopInBitOrderModel model;
@override
- State<ShopInBitCarFeeView> createState() => _ShopInBitCarFeeViewState();
+ ConsumerState<ShopInBitCarFeeView> createState() =>
+ _ShopInBitCarFeeViewState();
}
-class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
+class _ShopInBitCarFeeViewState extends ConsumerState<ShopInBitCarFeeView> {
late final TextEditingController _nameController;
late final TextEditingController _streetController;
late final TextEditingController _cityController;
@@ -179,7 +181,7 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
Future<void> _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<ShopInBitCarFeeView> {
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<ShopInBitCarFeeView> {
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<ShopInBitCarFeeView> {
);
}
- final resp = await ShopInBitService.instance.client
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
.createCarResearchInvoice(billing: billing);
if (resp.hasError || resp.value == null) {
@@ -264,7 +269,8 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
final invoice = resp.value!;
// Persist pending state so the user can resume if they close the dialog.
- // Sentinel ticketId; unique-replace index ensures at most one pending record.
+ // Sentinel ticketId; unique-replace index ensures at most one pending
+ // record.
widget.model.ticketId = "pending-car-research";
widget.model.carResearchInvoiceId = invoice.btcpayInvoice;
widget.model.isPendingPayment = true;
@@ -328,7 +334,9 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
// a fee field. Today the endpoint returns only {status, additional}, so
// we source the displayed amount from the BIP21 payment URIs instead.
try {
- final resp = await ShopInBitService.instance.client
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
.getCarResearchInvoiceStatus(invoice.btcpayInvoice);
if (resp.hasError || resp.value == null) {
Logging.instance.i(
@@ -471,9 +479,12 @@ class _ShopInBitCarFeeViewState extends State<ShopInBitCarFeeView> {
Assets.svg.chevronDown,
width: 12,
height: 6,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveSearchIconRight,
+ colorFilter: ColorFilter.mode(
+ Theme.of(
+ context,
+ ).extension<StackColors>()!.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 0073ee8..38db1c7 100644
--- a/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_car_research_payment_view.dart
@@ -10,9 +10,9 @@ import '../../db/isar/main_db.dart';
import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../providers/providers.dart';
import '../../route_generator.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
import '../../services/shopinbit/src/models/car_research.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/address_utils.dart';
@@ -22,16 +22,16 @@ import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
-import '../more_view/services_view.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
-import '../../widgets/stack_dialog.dart';
import '../../widgets/qr.dart';
import '../../widgets/rounded_white_container.dart';
+import '../../widgets/stack_dialog.dart';
+import '../more_view/services_view.dart';
import 'shopinbit_order_created.dart';
import 'shopinbit_send_from_view.dart';
import 'shopinbit_tickets_view.dart';
@@ -240,7 +240,8 @@ class _ShopInBitCarResearchPaymentViewState
showFloatingFlushBar(
type: FlushBarType.info,
message:
- "Payment not yet confirmed. Please wait a moment and try again.",
+ "Payment not yet confirmed. "
+ "Please wait a moment and try again.",
context: context,
),
);
@@ -345,7 +346,9 @@ class _ShopInBitCarResearchPaymentViewState
Future<void> _pollStatus() async {
try {
- final resp = await ShopInBitService.instance.client
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
.getCarResearchInvoiceStatus(widget.invoice.btcpayInvoice);
if (resp.hasError || resp.value == null) {
if (mounted) {
@@ -394,8 +397,9 @@ class _ShopInBitCarResearchPaymentViewState
if (_flowState == _PaymentFlowState.loggingPayment ||
_flowState == _PaymentFlowState.creatingRequest ||
_flowState == _PaymentFlowState.complete ||
- _flowState == _PaymentFlowState.error)
+ _flowState == _PaymentFlowState.error) {
return;
+ }
// Skip logCarResearchPayment if the fee was already logged.
final existingFeeTicket = widget.model.feeTicketNumber;
@@ -426,17 +430,22 @@ class _ShopInBitCarResearchPaymentViewState
setState(() => _flowState = _PaymentFlowState.creatingRequest);
_pollTimer?.cancel();
try {
- final customerKey = await ShopInBitService.instance.ensureCustomerKey();
+ final customerKey = await ref
+ .read(pShopinBitService)
+ .ensureCustomerKey();
final comment =
"${widget.model.requestDescription}\n\n"
"The Client paid the car research fee (#$existingFeeTicket)";
- final reqResp = await ShopInBitService.instance.client.createRequest(
- customerPseudonym: widget.model.displayName,
- externalCustomerKey: customerKey,
- serviceType: "car",
- comment: comment,
- deliveryCountry: widget.model.deliveryCountry,
- );
+ final reqResp = await ref
+ .read(pShopinBitService)
+ .client
+ .createRequest(
+ customerPseudonym: widget.model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car",
+ comment: comment,
+ deliveryCountry: widget.model.deliveryCountry,
+ );
if (reqResp.hasError || reqResp.value == null) {
if (mounted) {
setState(() => _flowState = _PaymentFlowState.error);
@@ -517,7 +526,9 @@ class _ShopInBitCarResearchPaymentViewState
_pollTimer?.cancel();
try {
- final logResp = await ShopInBitService.instance.client
+ final logResp = await ref
+ .read(pShopinBitService)
+ .client
.logCarResearchPayment(widget.invoice.btcpayInvoice);
if (logResp.hasError || logResp.value == null) {
if (mounted) {
@@ -535,7 +546,8 @@ class _ShopInBitCarResearchPaymentViewState
final feeResult = logResp.value!;
- // Persist feeTicketNumber on the existing model (a new DB row creates a spurious list entry).
+ // Persist feeTicketNumber on the existing model (a new DB row creates a
+ // spurious list entry).
widget.model.feeTicketNumber = feeResult.ticketNumber;
widget.model.needsCreateRequest = true;
await MainDB.instance.putShopInBitTicket(widget.model.toIsarTicket());
@@ -543,18 +555,21 @@ class _ShopInBitCarResearchPaymentViewState
if (!mounted) return;
setState(() => _flowState = _PaymentFlowState.creatingRequest);
- final customerKey = await ShopInBitService.instance.ensureCustomerKey();
+ final customerKey = await ref.read(pShopinBitService).ensureCustomerKey();
final comment =
"${widget.model.requestDescription}\n\n"
"The Client paid the car research fee (#${feeResult.ticketNumber})";
- final reqResp = await ShopInBitService.instance.client.createRequest(
- customerPseudonym: widget.model.displayName,
- externalCustomerKey: customerKey,
- serviceType: "car",
- comment: comment,
- deliveryCountry: widget.model.deliveryCountry,
- );
+ final reqResp = await ref
+ .read(pShopinBitService)
+ .client
+ .createRequest(
+ customerPseudonym: widget.model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car",
+ comment: comment,
+ deliveryCountry: widget.model.deliveryCountry,
+ );
if (reqResp.hasError || reqResp.value == null) {
// createRequest failed: fee receipt already persisted, show retry
@@ -645,13 +660,16 @@ class _ShopInBitCarResearchPaymentViewState
"${widget.model.requestDescription}\n\n"
"The Client paid the car research fee (#$feeTicketNumber)";
- final reqResp = await ShopInBitService.instance.client.createRequest(
- customerPseudonym: widget.model.displayName,
- externalCustomerKey: customerKey,
- serviceType: "car",
- comment: comment,
- deliveryCountry: widget.model.deliveryCountry,
- );
+ final reqResp = await ref
+ .read(pShopinBitService)
+ .client
+ .createRequest(
+ customerPseudonym: widget.model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car",
+ comment: comment,
+ deliveryCountry: widget.model.deliveryCountry,
+ );
if (reqResp.hasError || reqResp.value == null) {
if (mounted) {
diff --git a/lib/pages/shopinbit/shopinbit_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart
index f746b03..98946c1 100644
--- a/lib/pages/shopinbit/shopinbit_offer_view.dart
+++ b/lib/pages/shopinbit/shopinbit_offer_view.dart
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
@@ -15,7 +16,7 @@ import '../../widgets/loading_indicator.dart';
import '../../widgets/rounded_white_container.dart';
import 'shopinbit_shipping_view.dart';
-class ShopInBitOfferView extends StatefulWidget {
+class ShopInBitOfferView extends ConsumerStatefulWidget {
const ShopInBitOfferView({super.key, required this.model});
static const String routeName = "/shopInBitOffer";
@@ -23,10 +24,10 @@ class ShopInBitOfferView extends StatefulWidget {
final ShopInBitOrderModel model;
@override
- State<ShopInBitOfferView> createState() => _ShopInBitOfferViewState();
+ ConsumerState<ShopInBitOfferView> createState() => _ShopInBitOfferViewState();
}
-class _ShopInBitOfferViewState extends State<ShopInBitOfferView> {
+class _ShopInBitOfferViewState extends ConsumerState<ShopInBitOfferView> {
bool _loading = false;
@override
@@ -40,9 +41,10 @@ class _ShopInBitOfferViewState extends State<ShopInBitOfferView> {
Future<void> _loadOffer() async {
setState(() => _loading = true);
try {
- final resp = await ShopInBitService.instance.client.getTicketFull(
- widget.model.apiTicketId,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .getTicketFull(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null) {
final t = resp.value!;
widget.model.setOffer(
diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart
index 9813669..eea1f43 100644
--- a/lib/pages/shopinbit/shopinbit_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_payment_view.dart
@@ -11,9 +11,9 @@ import '../../app_config.dart';
import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../providers/providers.dart';
import '../../route_generator.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
import '../../services/shopinbit/src/models/payment.dart';
import '../../themes/coin_icon_provider.dart';
import '../../themes/stack_colors.dart';
@@ -108,9 +108,10 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
Future<void> _pollPayment() async {
try {
- final resp = await ShopInBitService.instance.client.getPayment(
- widget.model.apiTicketId,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .getPayment(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null && mounted) {
setState(() => _applyPaymentInfo(resp.value!));
if (_isTerminal) {
@@ -123,9 +124,10 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
Future<void> _loadPayment() async {
setState(() => _loading = true);
try {
- final resp = await ShopInBitService.instance.client.getPayment(
- widget.model.apiTicketId,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .getPayment(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null) {
_applyPaymentInfo(resp.value!);
}
@@ -142,10 +144,10 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
Future<void> _refreshInvoice() async {
setState(() => _loading = true);
try {
- final resp = await ShopInBitService.instance.client.getPayment(
- widget.model.apiTicketId,
- retry: true,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .getPayment(widget.model.apiTicketId, retry: true);
if (!resp.hasError && resp.value != null) {
_applyPaymentInfo(resp.value!);
}
@@ -160,9 +162,10 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
_pollTimer?.cancel();
setState(() => _loading = true);
try {
- final resp = await ShopInBitService.instance.client.getPayment(
- widget.model.apiTicketId,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .getPayment(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null && mounted) {
setState(() => _applyPaymentInfo(resp.value!));
final status = resp.value!.status;
diff --git a/lib/pages/shopinbit/shopinbit_send_from_view.dart b/lib/pages/shopinbit/shopinbit_send_from_view.dart
index 0060cf5..d2c08e2 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<ShopInBitSendFromCard> {
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 0682b74..50b6204 100644
--- a/lib/pages/shopinbit/shopinbit_settings_view.dart
+++ b/lib/pages/shopinbit/shopinbit_settings_view.dart
@@ -6,18 +6,22 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../notifications/show_flush_bar.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
+import '../../providers/providers.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
-import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
+import '../../utilities/util.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../widgets/desktop/desktop_dialog.dart';
+import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/rounded_container.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
-import '../../widgets/stack_text_field.dart';
+import '../../widgets/textfields/adaptive_text_field.dart';
class ShopInBitSettingsView extends ConsumerStatefulWidget {
const ShopInBitSettingsView({super.key});
@@ -31,11 +35,8 @@ class ShopInBitSettingsView extends ConsumerStatefulWidget {
class _ShopInBitSettingsViewState extends ConsumerState<ShopInBitSettingsView> {
final _manualKeyController = TextEditingController();
- final _manualKeyFocusNode = FocusNode();
final _verifyKeyController = TextEditingController();
- final _verifyKeyFocusNode = FocusNode();
- late final TextEditingController _displayNameController;
- late final FocusNode _displayNameFocusNode;
+ final _displayNameController = TextEditingController();
String? _currentKey;
bool _loading = false;
@@ -44,20 +45,29 @@ class _ShopInBitSettingsViewState extends ConsumerState<ShopInBitSettingsView> {
@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<ShopInBitSettingsView> {
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<ShopInBitSettingsView> {
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<ShopInBitSettingsView> {
setState(() => _loading = true);
try {
- await ShopInBitService.instance.setCustomerKey(newKey);
+ await ref.read(pShopinBitService).setCustomerKey(newKey);
setState(() {
_currentKey = newKey;
_manualKeyController.clear();
@@ -165,79 +175,165 @@ class _ShopInBitSettingsViewState extends ConsumerState<ShopInBitSettingsView> {
Future<bool?> _showChangeWarning() async {
final result = await showDialog<bool>(
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<StackColors>()!.warningBackground,
- child: SelectableText(
- _currentKey!,
- style: STextStyles.smallMed14(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.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<StackColors>()!
- .getSecondaryEnabledButtonStyle(context),
- onPressed: () => Navigator.of(context).pop(false),
- child: Text(
- "Cancel",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(32),
+ child: Text(
+ "Save your current key",
+ style: STextStyles.desktopH3(context),
),
),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ right: 32,
+ bottom: 32,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Your current customer key is:",
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ ),
+ const SizedBox(height: 8),
+ RoundedWhiteContainer(
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle6,
+ child: SelectableText(
+ _currentKey!,
+ style: STextStyles.desktopTextSmall(context),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ "Changing your key will disconnect you from "
+ "existing ShopinBit requests. Make sure "
+ "you have saved your current key before "
+ "proceeding.",
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ ),
+ const SizedBox(height: 32),
+ Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ buttonHeight: ButtonHeight.l,
+ onPressed: () => Navigator.of(
+ context,
+ rootNavigator: true,
+ ).pop(false),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: PrimaryButton(
+ label: "I saved my key",
+ buttonHeight: ButtonHeight.l,
+ onPressed: () => Navigator.of(
+ context,
+ rootNavigator: true,
+ ).pop(null),
+ ),
+ ),
+ ],
+ ),
+ ],
),
),
- const SizedBox(width: 8),
- Expanded(
- child: TextButton(
- style: Theme.of(context)
- .extension<StackColors>()!
- .getPrimaryEnabledButtonStyle(context),
- onPressed: () => Navigator.of(context).pop(null),
- child: Text(
- "I saved my key",
- style: STextStyles.button(context),
+ ],
+ ),
+ );
+ } else {
+ return StackDialogBase(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Save your current key",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ const SizedBox(height: 8),
+ SelectableText(
+ "Your current customer key is:",
+ style: STextStyles.smallMed14(context),
+ ),
+ const SizedBox(height: 8),
+ RoundedContainer(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.warningBackground,
+ child: SelectableText(
+ _currentKey!,
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.warningForeground,
),
),
),
+ const SizedBox(height: 8),
+ SelectableText(
+ "Changing your key will disconnect you from "
+ "existing ShopinBit conversations. Make sure "
+ "you have saved your current key before "
+ "proceeding.",
+ style: STextStyles.smallMed14(context),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ Expanded(
+ child: TextButton(
+ style: Theme.of(context)
+ .extension<StackColors>()!
+ .getSecondaryEnabledButtonStyle(context),
+ onPressed: () => Navigator.of(context).pop(false),
+ child: Text(
+ "Cancel",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorDark,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: TextButton(
+ style: Theme.of(context)
+ .extension<StackColors>()!
+ .getPrimaryEnabledButtonStyle(context),
+ onPressed: () => Navigator.of(context).pop(null),
+ child: Text(
+ "I saved my key",
+ style: STextStyles.button(context),
+ ),
+ ),
+ ),
+ ],
+ ),
],
),
- ],
- ),
- ),
+ );
+ }
+ },
);
if (result == false || !mounted) return false;
@@ -250,81 +346,150 @@ class _ShopInBitSettingsViewState extends ConsumerState<ShopInBitSettingsView> {
return showDialog<bool>(
context: context,
barrierDismissible: true,
- builder: (ctx) {
+ builder: (context) {
return StatefulBuilder(
builder: (ctx, setDialogState) {
final matches = _verifyKeyController.text.trim() == _currentKey;
- return StackDialogBase(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text("Verify your key", style: STextStyles.pageTitleH2(ctx)),
- const SizedBox(height: 8),
- Text(
- "Enter your current customer key to "
- "confirm you have saved it.",
- style: STextStyles.smallMed14(ctx),
- ),
- const SizedBox(height: 16),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
+
+ // TODO: this conditional can probably be merged when we have time
+ if (Util.isDesktop) {
+ return DesktopDialog(
+ maxWidth: 550,
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(32),
+ child: Text(
+ "Verify your key",
+ style: STextStyles.desktopH3(ctx),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
),
- child: TextField(
- controller: _verifyKeyController,
- focusNode: _verifyKeyFocusNode,
- style: STextStyles.field(ctx),
- decoration: standardInputDecoration(
- "Enter current key",
- _verifyKeyFocusNode,
- ctx,
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ right: 32,
+ bottom: 32,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Enter your current customer key to "
+ "confirm you have saved it.",
+ style: STextStyles.desktopTextExtraExtraSmall(ctx),
+ ),
+ const SizedBox(height: 16),
+ AdaptiveTextField(
+ labelText: "Enter current key",
+ controller: _verifyKeyController,
+ onChangedComprehensive: (_) =>
+ setDialogState(() {}),
+ ),
+ const SizedBox(height: 32),
+ Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ buttonHeight: ButtonHeight.l,
+ onPressed: () => Navigator.of(
+ ctx,
+ rootNavigator: true,
+ ).pop(false),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: PrimaryButton(
+ label: "Confirm",
+ buttonHeight: ButtonHeight.l,
+ enabled: matches,
+ onPressed: () => Navigator.of(
+ ctx,
+ rootNavigator: true,
+ ).pop(true),
+ ),
+ ),
+ ],
+ ),
+ ],
),
- onChanged: (_) => setDialogState(() {}),
),
- ),
- const SizedBox(height: 20),
- Row(
- children: [
- Expanded(
- child: TextButton(
- style: Theme.of(ctx)
- .extension<StackColors>()!
- .getSecondaryEnabledButtonStyle(ctx),
- onPressed: () => Navigator.of(ctx).pop(false),
- child: Text(
- "Cancel",
- style: STextStyles.button(ctx).copyWith(
- color: Theme.of(
- ctx,
- ).extension<StackColors>()!.accentColorDark,
+ ],
+ ),
+ );
+ } else {
+ return StackDialogBase(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Verify your key",
+ style: STextStyles.pageTitleH2(ctx),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "Enter your current customer key to "
+ "confirm you have saved it.",
+ style: STextStyles.smallMed14(ctx),
+ ),
+ const SizedBox(height: 16),
+ AdaptiveTextField(
+ labelText: "Enter current key",
+ controller: _verifyKeyController,
+ onChangedComprehensive: (_) => setDialogState(() {}),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ Expanded(
+ child: TextButton(
+ style: Theme.of(ctx)
+ .extension<StackColors>()!
+ .getSecondaryEnabledButtonStyle(ctx),
+ onPressed: () => Navigator.of(ctx).pop(false),
+ child: Text(
+ "Cancel",
+ style: STextStyles.button(ctx).copyWith(
+ color: Theme.of(
+ ctx,
+ ).extension<StackColors>()!.accentColorDark,
+ ),
),
),
),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: TextButton(
- style: matches
- ? Theme.of(ctx)
- .extension<StackColors>()!
- .getPrimaryEnabledButtonStyle(ctx)
- : Theme.of(ctx)
- .extension<StackColors>()!
- .getPrimaryDisabledButtonStyle(ctx),
- onPressed: matches
- ? () => Navigator.of(ctx).pop(true)
- : null,
- child: Text(
- "Confirm",
- style: STextStyles.button(ctx),
+ const SizedBox(width: 8),
+ Expanded(
+ child: TextButton(
+ style: matches
+ ? Theme.of(ctx)
+ .extension<StackColors>()!
+ .getPrimaryEnabledButtonStyle(ctx)
+ : Theme.of(ctx)
+ .extension<StackColors>()!
+ .getPrimaryDisabledButtonStyle(ctx),
+ onPressed: matches
+ ? () => Navigator.of(ctx).pop(true)
+ : null,
+ child: Text(
+ "Confirm",
+ style: STextStyles.button(ctx),
+ ),
),
),
- ),
- ],
- ),
- ],
- ),
- );
+ ],
+ ),
+ ],
+ ),
+ );
+ }
},
);
},
@@ -333,213 +498,388 @@ class _ShopInBitSettingsViewState extends ConsumerState<ShopInBitSettingsView> {
@override
Widget build(BuildContext context) {
- return Background(
- child: Scaffold(
- backgroundColor: Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
- ),
- title: Text("ShopinBit", style: STextStyles.navBarTitle(context)),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.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<StackColors>()!.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<StackColors>()!.textDark3,
),
- const SizedBox(height: 16),
- if (_currentKey != null) ...[
- RoundedContainer(
- color: Theme.of(context)
- .extension<StackColors>()!
- .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<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Text("ShopinBit", style: STextStyles.navBarTitle(context)),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.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<StackColors>()!
+ .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<StackColors>()!
- .textDark3,
+ if (mounted) {
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message:
+ "Key copied to clipboard",
+ context: context,
+ ),
+ );
+ }
+ },
+ child: SvgPicture.asset(
+ Assets.svg.copy,
+ width: 20,
+ height: 20,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textDark3,
+ ),
),
- ),
- ],
+ ],
+ ),
),
+ ] else
+ Text(
+ "No key set",
+ style: STextStyles.itemSubtitle(
+ context,
+ ),
+ ),
+ const SizedBox(height: 16),
+ PrimaryButton(
+ label: _currentKey == null
+ ? "Generate key"
+ : "Generate new key",
+ enabled: !_loading,
+ onPressed: _generate,
),
- ] else
- Text(
- "No key set",
- style: STextStyles.itemSubtitle(context),
- ),
- const SizedBox(height: 16),
- PrimaryButton(
- label: _currentKey == null
- ? "Generate key"
- : "Generate new key",
- enabled: !_loading,
- onPressed: _generate,
- ),
- ],
+ ],
+ ),
),
- ),
- const SizedBox(height: 12),
- RoundedWhiteContainer(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Restore key",
- style: STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 8),
- Text(
- "Enter a previously saved customer "
- "key to restore access to your "
- "ShopinBit conversations.",
- style: STextStyles.itemSubtitle12(context),
- ),
- const SizedBox(height: 12),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
+ const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Restore key",
+ style: STextStyles.titleBold12(context),
),
- child: TextField(
- controller: _manualKeyController,
- focusNode: _manualKeyFocusNode,
- style: STextStyles.field(context),
- decoration: standardInputDecoration(
- "Enter customer key",
- _manualKeyFocusNode,
+ const SizedBox(height: 8),
+ Text(
+ "Enter a previously saved customer "
+ "key to restore access to your "
+ "ShopinBit conversations.",
+ style: STextStyles.itemSubtitle12(
context,
),
- onChanged: (_) => setState(() {}),
),
- ),
- const SizedBox(height: 12),
- PrimaryButton(
- label: "Set key",
- enabled:
- !_loading &&
- _manualKeyController.text
- .trim()
- .isNotEmpty,
- onPressed: _setManualKey,
- ),
- ],
+ const SizedBox(height: 12),
+ AdaptiveTextField(
+ labelText: "Enter customer key",
+ controller: _manualKeyController,
+ onChangedComprehensive: (_) =>
+ setState(() {}),
+ ),
+ const SizedBox(height: 12),
+ PrimaryButton(
+ label: "Set key",
+ enabled:
+ !_loading &&
+ _manualKeyController.text
+ .trim()
+ .isNotEmpty,
+ onPressed: _setManualKey,
+ ),
+ ],
+ ),
),
- ),
- const SizedBox(height: 12),
- RoundedWhiteContainer(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Display Name",
- style: STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 8),
- Text(
- "The name ShopinBit staff will see "
- "when communicating with you.",
- style: STextStyles.itemSubtitle12(context),
- ),
- const SizedBox(height: 12),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
+ const SizedBox(height: 12),
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Display Name",
+ style: STextStyles.titleBold12(context),
),
- child: TextField(
- controller: _displayNameController,
- focusNode: _displayNameFocusNode,
- style: STextStyles.field(context),
- decoration: standardInputDecoration(
- "Display name",
- _displayNameFocusNode,
+ const SizedBox(height: 8),
+ Text(
+ "The name ShopinBit staff will see "
+ "when communicating with you.",
+ style: STextStyles.itemSubtitle12(
context,
),
- onChanged: (_) => setState(() {}),
),
- ),
- const SizedBox(height: 12),
- PrimaryButton(
- label: "Save",
- enabled:
- !_savingName &&
- _displayNameController.text
- .trim()
- .isNotEmpty,
- onPressed: _saveDisplayName,
- ),
- ],
+ const SizedBox(height: 12),
+ AdaptiveTextField(
+ labelText: "Display name",
+ controller: _displayNameController,
+ onChangedComprehensive: (_) =>
+ setState(() {}),
+ ),
+ const SizedBox(height: 12),
+ PrimaryButton(
+ label: "Save",
+ enabled:
+ !_savingName &&
+ _displayNameController.text
+ .trim()
+ .isNotEmpty,
+ onPressed: _saveDisplayName,
+ ),
+ ],
+ ),
),
- ),
- const SizedBox(height: 12),
- ],
+ const SizedBox(height: 12),
+ ],
+ ),
),
),
),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
- ),
- );
+ );
+ }
}
}
diff --git a/lib/pages/shopinbit/shopinbit_setup_view.dart b/lib/pages/shopinbit/shopinbit_setup_view.dart
index 5566d53..1ce525f 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<ShopInBitSetupView> createState() => _ShopInBitSetupViewState();
+ ConsumerState<ShopInBitSetupView> createState() => _ShopInBitSetupViewState();
}
-class _ShopInBitSetupViewState extends State<ShopInBitSetupView> {
+class _ShopInBitSetupViewState extends ConsumerState<ShopInBitSetupView> {
late final Future<String> _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<void> _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<ShopInBitSetupView> {
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 013b276..03ae923 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<ShopInBitShippingView> createState() => _ShopInBitShippingViewState();
+ ConsumerState<ShopInBitShippingView> createState() =>
+ _ShopInBitShippingViewState();
}
-class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
+class _ShopInBitShippingViewState extends ConsumerState<ShopInBitShippingView> {
late final TextEditingController _nameController;
late final TextEditingController _streetController;
late final TextEditingController _cityController;
@@ -150,7 +152,7 @@ class _ShopInBitShippingViewState extends State<ShopInBitShippingView> {
Future<void> _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<ShopInBitShippingView> {
);
}
- final resp = await ShopInBitService.instance.client.submitAddress(
- widget.model.apiTicketId,
- shipping: Address(
- firstName: firstName,
- lastName: lastName,
- street: street,
- zip: postalCode,
- city: city,
- country: country,
- ),
- billing: billingAddress,
- );
+ final resp = await ref
+ .read(pShopinBitService)
+ .client
+ .submitAddress(
+ widget.model.apiTicketId,
+ shipping: Address(
+ firstName: firstName,
+ lastName: lastName,
+ street: street,
+ zip: postalCode,
+ city: city,
+ country: country,
+ ),
+ billing: billingAddress,
+ );
if (resp.hasError) {
// Sandbox may fail here; continue anyway.
diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart
index b69cc87..23403ce 100644
--- a/lib/pages/shopinbit/shopinbit_step_2.dart
+++ b/lib/pages/shopinbit/shopinbit_step_2.dart
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/providers.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
@@ -18,7 +19,7 @@ import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_3.dart';
import 'shopinbit_step_4.dart';
-class ShopInBitStep2 extends StatefulWidget {
+class ShopInBitStep2 extends ConsumerStatefulWidget {
const ShopInBitStep2({super.key, required this.model});
static const String routeName = "/shopInBitStep2";
@@ -26,23 +27,26 @@ class ShopInBitStep2 extends StatefulWidget {
final ShopInBitOrderModel model;
@override
- State<ShopInBitStep2> createState() => _ShopInBitStep2State();
+ ConsumerState<ShopInBitStep2> createState() => _ShopInBitStep2State();
}
-class _ShopInBitStep2State extends State<ShopInBitStep2> {
+class _ShopInBitStep2State extends ConsumerState<ShopInBitStep2> {
ShopInBitCategory? _selected;
- void _continue() {
+ Future<void> _continue() async {
widget.model.category = _selected;
- final skipGuidelines = ShopInBitService.instance.loadGuidelinesAccepted();
+ final skipGuidelines =
+ (await ref.read(pSharedDrift).shopinBitSettingsDao.getSettings())
+ .guidelinesAccepted;
+ if (!mounted) return;
if (skipGuidelines) {
widget.model.guidelinesAccepted = true;
- Navigator.of(
+ await Navigator.of(
context,
).pushNamed(ShopInBitStep4.routeName, arguments: widget.model);
} else {
- Navigator.of(
+ await Navigator.of(
context,
).pushNamed(ShopInBitStep3.routeName, arguments: widget.model);
}
diff --git a/lib/pages/shopinbit/shopinbit_step_3.dart b/lib/pages/shopinbit/shopinbit_step_3.dart
index 4f6b799..f84d487 100644
--- a/lib/pages/shopinbit/shopinbit_step_3.dart
+++ b/lib/pages/shopinbit/shopinbit_step_3.dart
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/providers.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
@@ -14,7 +15,7 @@ import '../../widgets/rounded_white_container.dart';
import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_4.dart';
-class ShopInBitStep3 extends StatefulWidget {
+class ShopInBitStep3 extends ConsumerStatefulWidget {
const ShopInBitStep3({super.key, required this.model});
static const String routeName = "/shopInBitStep3";
@@ -22,10 +23,10 @@ class ShopInBitStep3 extends StatefulWidget {
final ShopInBitOrderModel model;
@override
- State<ShopInBitStep3> createState() => _ShopInBitStep3State();
+ ConsumerState<ShopInBitStep3> createState() => _ShopInBitStep3State();
}
-class _ShopInBitStep3State extends State<ShopInBitStep3> {
+class _ShopInBitStep3State extends ConsumerState<ShopInBitStep3> {
bool _agreed = false;
String _guidelinesText() {
@@ -76,7 +77,7 @@ class _ShopInBitStep3State extends State<ShopInBitStep3> {
void _continue() {
widget.model.guidelinesAccepted = true;
// Persist acceptance.
- ShopInBitService.instance.setGuidelinesAccepted(true);
+ ref.read(pSharedDrift).shopinBitSettingsDao.setGuidelinesAccepted(true);
Navigator.of(
context,
diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
index 671ee89..d8c8123 100644
--- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart
+++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../db/isar/main_db.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../notifications/show_flush_bar.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
@@ -19,7 +20,7 @@ import '../../widgets/loading_indicator.dart';
import '../../widgets/rounded_white_container.dart';
import 'shopinbit_offer_view.dart';
-class ShopInBitTicketDetail extends StatefulWidget {
+class ShopInBitTicketDetail extends ConsumerStatefulWidget {
const ShopInBitTicketDetail({super.key, required this.model});
static const String routeName = "/shopInBitTicketDetail";
@@ -27,10 +28,11 @@ class ShopInBitTicketDetail extends StatefulWidget {
final ShopInBitOrderModel model;
@override
- State<ShopInBitTicketDetail> createState() => _ShopInBitTicketDetailState();
+ ConsumerState<ShopInBitTicketDetail> createState() =>
+ _ShopInBitTicketDetailState();
}
-class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
+class _ShopInBitTicketDetailState extends ConsumerState<ShopInBitTicketDetail> {
late final TextEditingController _messageController;
bool _sending = false;
@@ -65,7 +67,7 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
Future<void> _loadFromApi() async {
setState(() => _loading = true);
try {
- final client = ShopInBitService.instance.client;
+ final client = ref.read(pShopinBitService).client;
final id = widget.model.apiTicketId;
final messagesResp = await client.getMessages(id);
@@ -129,10 +131,10 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
try {
if (widget.model.apiTicketId != 0) {
- await ShopInBitService.instance.client.sendMessage(
- widget.model.apiTicketId,
- text,
- );
+ await ref
+ .read(pShopinBitService)
+ .client
+ .sendMessage(widget.model.apiTicketId, text);
// Reload messages from API to get accurate state
await _loadFromApi();
}
@@ -152,18 +154,21 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
try {
final model = widget.model;
- final customerKey = await ShopInBitService.instance.ensureCustomerKey();
+ final customerKey = await ref.read(pShopinBitService).ensureCustomerKey();
final comment =
"${model.requestDescription}\n\n"
"The Client paid the car research fee (#${model.feeTicketNumber})";
- final reqResp = await ShopInBitService.instance.client.createRequest(
- customerPseudonym: model.displayName,
- externalCustomerKey: customerKey,
- serviceType: "car_research",
- comment: comment,
- deliveryCountry: model.deliveryCountry,
- );
+ final reqResp = await ref
+ .read(pShopinBitService)
+ .client
+ .createRequest(
+ customerPseudonym: model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car_research",
+ comment: comment,
+ deliveryCountry: model.deliveryCountry,
+ );
if (reqResp.hasError || reqResp.value == null) {
if (mounted) {
diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart
index 0ff73b6..a7a1402 100644
--- a/lib/pages/shopinbit/shopinbit_tickets_view.dart
+++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart
@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../db/isar/main_db.dart';
import '../../models/isar/models/shopinbit_ticket.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
-import '../../services/shopinbit/shopinbit_service.dart';
+import '../../providers/global/shopin_bit_service_provider.dart';
import '../../services/shopinbit/src/models/car_research.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
@@ -21,16 +22,17 @@ import 'shopinbit_car_fee_view.dart';
import 'shopinbit_car_research_payment_view.dart';
import 'shopinbit_ticket_detail.dart';
-class ShopInBitTicketsView extends StatefulWidget {
+class ShopInBitTicketsView extends ConsumerStatefulWidget {
const ShopInBitTicketsView({super.key});
static const String routeName = "/shopInBitTickets";
@override
- State<ShopInBitTicketsView> createState() => _ShopInBitTicketsViewState();
+ ConsumerState<ShopInBitTicketsView> createState() =>
+ _ShopInBitTicketsViewState();
}
-class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
+class _ShopInBitTicketsViewState extends ConsumerState<ShopInBitTicketsView> {
List<ShopInBitOrderModel> _tickets = [];
bool _syncing = false;
ShopInBitTicket? _pendingTicket;
@@ -112,7 +114,7 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
Future<void> _syncFromApi() async {
setState(() => _syncing = true);
try {
- final service = ShopInBitService.instance;
+ final service = ref.read(pShopinBitService);
final customerKey = await service.ensureCustomerKey();
final resp = await service.client.getTicketsByCustomer(customerKey);
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
index 282d826..993a773 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_concierge_form.dart
@@ -1,7 +1,9 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
import "../../../models/shopinbit/shopinbit_order_model.dart";
+import "../../../providers/global/shopin_bit_service_provider.dart";
import "../../../utilities/util.dart";
import "shopinbit_country_picker.dart";
import "shopinbit_labeled_checkbox.dart";
@@ -17,16 +19,18 @@ const List<String> _conciergeConditions = ["NEW", "USED"];
const int _minConciergeBudget = 1000;
const int _maxConciergeBudget = 100000;
-class ShopInBitConciergeForm extends StatefulWidget {
+class ShopInBitConciergeForm extends ConsumerStatefulWidget {
const ShopInBitConciergeForm({super.key, required this.model});
final ShopInBitOrderModel model;
@override
- State<ShopInBitConciergeForm> createState() => _ShopInBitConciergeFormState();
+ ConsumerState<ShopInBitConciergeForm> createState() =>
+ _ShopInBitConciergeFormState();
}
-class _ShopInBitConciergeFormState extends State<ShopInBitConciergeForm> {
+class _ShopInBitConciergeFormState
+ extends ConsumerState<ShopInBitConciergeForm> {
final TextEditingController _whatToPurchaseController =
TextEditingController();
final FocusNode _whatToPurchaseFocusNode = FocusNode();
@@ -103,7 +107,11 @@ class _ShopInBitConciergeFormState extends State<ShopInBitConciergeForm> {
..deliveryCountry = countryIso;
try {
- await submitShopInBitRequest(context, widget.model);
+ await submitShopInBitRequest(
+ context,
+ widget.model,
+ ref.read(pShopinBitService),
+ );
} finally {
if (mounted) setState(() => _submitting = false);
}
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
index f5ae405..f0feb9d 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_country_picker.dart
@@ -1,15 +1,16 @@
import "package:dropdown_button2/dropdown_button2.dart";
import "package:flutter/material.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:flutter_svg/svg.dart";
-import "../../../services/shopinbit/shopinbit_service.dart";
+import "../../../providers/global/shopin_bit_service_provider.dart";
import "../../../themes/stack_colors.dart";
import "../../../utilities/assets.dart";
import "../../../utilities/constants.dart";
import "../../../utilities/text_styles.dart";
import "../../../utilities/util.dart";
-class ShopInBitCountryPicker extends StatefulWidget {
+class ShopInBitCountryPicker extends ConsumerStatefulWidget {
const ShopInBitCountryPicker({
super.key,
required this.selectedIso,
@@ -22,10 +23,12 @@ class ShopInBitCountryPicker extends StatefulWidget {
final String hintText;
@override
- State<ShopInBitCountryPicker> createState() => _ShopInBitCountryPickerState();
+ ConsumerState<ShopInBitCountryPicker> createState() =>
+ _ShopInBitCountryPickerState();
}
-class _ShopInBitCountryPickerState extends State<ShopInBitCountryPicker> {
+class _ShopInBitCountryPickerState
+ extends ConsumerState<ShopInBitCountryPicker> {
final TextEditingController _searchController = TextEditingController();
List<Map<String, dynamic>> _countries = [];
bool _loading = false;
@@ -45,7 +48,7 @@ class _ShopInBitCountryPickerState extends State<ShopInBitCountryPicker> {
Future<void> _fetchCountries() async {
setState(() => _loading = true);
try {
- final resp = await ShopInBitService.instance.client.getCountries();
+ final resp = await ref.read(pShopinBitService).client.getCountries();
if (resp.hasError || resp.value == null) return;
_countries = resp.value!;
if (widget.selectedIso != null &&
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart
index f31741f..015e94d 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_generic_form.dart
@@ -1,6 +1,8 @@
import "package:flutter/material.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
import "../../../models/shopinbit/shopinbit_order_model.dart";
+import "../../../providers/global/shopin_bit_service_provider.dart";
import "../../../utilities/util.dart";
import "shopinbit_country_picker.dart";
import "shopinbit_privacy_checkbox.dart";
@@ -14,16 +16,17 @@ import "shopinbit_step4_text_field.dart";
///
/// Note: the original code used the travel copy for this fallback; that
/// behaviour is preserved here.
-class ShopInBitGenericForm extends StatefulWidget {
+class ShopInBitGenericForm extends ConsumerStatefulWidget {
const ShopInBitGenericForm({super.key, required this.model});
final ShopInBitOrderModel model;
@override
- State<ShopInBitGenericForm> createState() => _ShopInBitGenericFormState();
+ ConsumerState<ShopInBitGenericForm> createState() =>
+ _ShopInBitGenericFormState();
}
-class _ShopInBitGenericFormState extends State<ShopInBitGenericForm> {
+class _ShopInBitGenericFormState extends ConsumerState<ShopInBitGenericForm> {
late final TextEditingController _descriptionController;
final FocusNode _descriptionFocusNode = FocusNode();
@@ -63,7 +66,11 @@ class _ShopInBitGenericFormState extends State<ShopInBitGenericForm> {
..requestDescription = _descriptionController.text.trim()
..deliveryCountry = _selectedCountryIso!;
try {
- await submitShopInBitRequest(context, widget.model);
+ await submitShopInBitRequest(
+ context,
+ widget.model,
+ ref.read(pShopinBitService),
+ );
} finally {
if (mounted) setState(() => _submitting = false);
}
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart
index ede1424..1f0e798 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_step4_submit.dart
@@ -17,9 +17,9 @@ import "../shopinbit_order_created.dart";
Future<void> submitShopInBitRequest(
BuildContext context,
ShopInBitOrderModel model,
+ ShopInBitService service,
) async {
try {
- final ShopInBitService service = ShopInBitService.instance;
final String customerKey = await service.ensureCustomerKey();
assert(
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
index bfddd18..b84885a 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_travel_form.dart
@@ -1,7 +1,9 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
import "../../../models/shopinbit/shopinbit_order_model.dart";
+import "../../../providers/global/shopin_bit_service_provider.dart";
import "../../../utilities/text_styles.dart";
import "../../../utilities/util.dart";
import "shopinbit_country_picker.dart";
@@ -54,16 +56,17 @@ const int _minArrangementDetailsLength = 10;
/// Travel request form. Collects arrangement type, departure / destinations,
/// dates (either exact or flexible), travelers and budget, then submits via
/// the shared submit helper.
-class ShopInBitTravelForm extends StatefulWidget {
+class ShopInBitTravelForm extends ConsumerStatefulWidget {
const ShopInBitTravelForm({super.key, required this.model});
final ShopInBitOrderModel model;
@override
- State<ShopInBitTravelForm> createState() => _ShopInBitTravelFormState();
+ ConsumerState<ShopInBitTravelForm> createState() =>
+ _ShopInBitTravelFormState();
}
-class _ShopInBitTravelFormState extends State<ShopInBitTravelForm> {
+class _ShopInBitTravelFormState extends ConsumerState<ShopInBitTravelForm> {
final TextEditingController _arrangementDetailsController =
TextEditingController();
final FocusNode _arrangementDetailsFocusNode = FocusNode();
@@ -271,7 +274,11 @@ class _ShopInBitTravelFormState extends State<ShopInBitTravelForm> {
// structured comment field.
..deliveryCountry = "DE";
try {
- await submitShopInBitRequest(context, widget.model);
+ await submitShopInBitRequest(
+ context,
+ widget.model,
+ ref.read(pShopinBitService),
+ );
} finally {
if (mounted) setState(() => _submitting = false);
}
diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
index 956a77c..b42fb08 100644
--- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
+++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
@@ -11,8 +11,9 @@ import '../../../models/shopinbit/shopinbit_order_model.dart';
import '../../../notifications/show_flush_bar.dart';
import '../../../pages/shopinbit/shopinbit_step_1.dart';
import '../../../pages/shopinbit/shopinbit_tickets_view.dart';
+import '../../../providers/db/drift_provider.dart';
import '../../../providers/desktop/current_desktop_menu_item.dart';
-import '../../../services/shopinbit/shopinbit_service.dart';
+import '../../../providers/global/shopin_bit_service_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/text_styles.dart';
@@ -90,12 +91,16 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
return shouldContinue ?? false;
}
- Future<void> _showShopDialog(BuildContext context) async {
- final service = ShopInBitService.instance;
+ Future<void> _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<bool>(
context: context,
@@ -106,13 +111,13 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
isFirstRun = true;
} else {
// Returning user: restore display name.
- final savedName = service.loadDisplayName();
+ final savedName = settings.displayName;
if (savedName != null && savedName.isNotEmpty) {
model.displayName = savedName;
}
}
- if (!context.mounted) return;
+ if (!mounted) return;
if (isFirstRun) {
// First run: show service overview then go directly to Step2
@@ -239,7 +244,7 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
buttonHeight: ButtonHeight.m,
enabled: true,
label: "Shop with ShopinBit",
- onPressed: () => _showShopDialog(context),
+ onPressed: _showShopDialog,
),
const SizedBox(width: 16),
Builder(
@@ -296,29 +301,41 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
}
}
-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<String> _keyFuture;
- late final TextEditingController _nameController;
+ final TextEditingController _nameController = TextEditingController();
bool get _canContinue => _nameController.text.trim().isNotEmpty;
@override
void initState() {
super.initState();
- _keyFuture = ShopInBitService.instance.ensureCustomerKey();
- final existingName = ShopInBitService.instance.loadDisplayName();
- _nameController = TextEditingController(text: existingName ?? '');
+ _keyFuture = ref.read(pShopinBitService).ensureCustomerKey();
+
+ // not the greatest solution but its the least invasive with the current
+ // ui code impl
+ () async {
+ final settings = await ref
+ .read(pSharedDrift)
+ .shopinBitSettingsDao
+ .getSettings();
+ if (mounted) {
+ setState(() {
+ _nameController.text = settings.displayName ?? "";
+ });
+ }
+ }();
}
@override
@@ -330,8 +347,9 @@ class _ShopInBitDesktopSetupDialogState
Future<void> _completeSetup() async {
final name = _nameController.text.trim();
widget.model.displayName = name;
- await ShopInBitService.instance.setDisplayName(name);
- await ShopInBitService.instance.setSetupComplete(true);
+ final dao = ref.read(pSharedDrift).shopinBitSettingsDao;
+ await dao.setDisplayName(name);
+ await dao.setSetupComplete(true);
if (mounted) {
Navigator.of(context, rootNavigator: true).pop(true);
}
diff --git a/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart
index 69b6dff..b693f5d 100644
--- a/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart
+++ b/lib/pages_desktop_specific/services/shopin_bit/sub_widgets/desktop_shopin_bit_first_run.dart
@@ -19,45 +19,49 @@ class DesktopShopinBitFirstRun extends StatelessWidget {
return SDialog(
child: SizedBox(
width: 580,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text("ShopinBit", style: STextStyles.desktopH2(context)),
- const SizedBox(height: 16),
- RichText(
- text: TextSpan(
- style: STextStyles.desktopTextSmall(context),
- children: const [
- TextSpan(
- text:
- "Please note the following before proceeding:"
- "\n\n\u2022 Minimum order amount: 1,000 EUR"
- "\n\u2022 Service fee: 10% of the order total",
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text("ShopinBit", style: STextStyles.desktopH2(context)),
+ const SizedBox(height: 24),
+ RichText(
+ text: TextSpan(
+ style: STextStyles.desktopTextSmall(context),
+ children: const [
+ TextSpan(
+ text:
+ "Please note the following before proceeding:"
+ "\n\n\u2022 Minimum order amount: 1,000 EUR"
+ "\n\u2022 Service fee: 10% of the order total",
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SecondaryButton(
+ width: 220,
+ buttonHeight: ButtonHeight.l,
+ label: "Cancel",
+ onPressed: Navigator.of(context).pop,
+ ),
+ PrimaryButton(
+ width: 220,
+ buttonHeight: ButtonHeight.l,
+ label: "Continue",
+ onPressed: () => Navigator.of(context).pushReplacementNamed(
+ ShopInBitStep1.routeName,
+ arguments: model,
+ ),
),
],
),
- ),
- const Spacer(),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- SecondaryButton(
- width: 220,
- buttonHeight: ButtonHeight.l,
- label: "Cancel",
- onPressed: Navigator.of(context).pop,
- ),
- PrimaryButton(
- width: 220,
- buttonHeight: ButtonHeight.l,
- label: "Continue",
- onPressed: () => Navigator.of(
- context,
- ).pushNamed(ShopInBitStep1.routeName, arguments: model),
- ),
- ],
- ),
- ],
+ ],
+ ),
),
),
);
diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart
index 4247186..ee2c423 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<DesktopSettingsView> {
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 146243c..0000000
--- 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<ShopInBitDesktopSettings> createState() =>
- _ShopInBitDesktopSettingsState();
-}
-
-class _ShopInBitDesktopSettingsState
- extends ConsumerState<ShopInBitDesktopSettings> {
- 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<void> _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<void> _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<void> _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<bool?> _showChangeWarning() async {
- final result = await showDialog<bool>(
- 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<StackColors>()!.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<bool?> _showVerifyDialog() async {
- _verifyKeyController.clear();
- return showDialog<bool>(
- 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<StackColors>()!.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<StackColors>()!.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 658dd5b..9f6ea4c 100644
--- a/lib/providers/db/drift_provider.dart
+++ b/lib/providers/db/drift_provider.dart
@@ -10,8 +10,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../../db/drift/database.dart';
+import '../../db/drift/database.dart' show WalletDatabase, Drift;
+import '../../db/drift/shared_database.dart' show SharedDrift;
final pDrift = Provider.family<WalletDatabase, String>(
(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 0000000..9f9c422
--- /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 d08cdb1..5aa2396 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -254,7 +254,6 @@ import 'pages_desktop_specific/settings/settings_menu/desktop_support_view.dart'
import 'pages_desktop_specific/settings/settings_menu/language_settings/language_settings.dart';
import 'pages_desktop_specific/settings/settings_menu/nodes_settings.dart';
import 'pages_desktop_specific/settings/settings_menu/security_settings.dart';
-import 'pages_desktop_specific/settings/settings_menu/shopinbit_settings.dart';
import 'pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
import 'pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart';
import 'pages_desktop_specific/spark_coins/spark_coins_view.dart';
@@ -2737,13 +2736,6 @@ class RouteGenerator {
settings: RouteSettings(name: settings.name),
);
- case ShopInBitDesktopSettings.routeName:
- return getRoute(
- shouldUseMaterialRoute: useMaterialPageRoute,
- builder: (_) => const ShopInBitDesktopSettings(),
- settings: RouteSettings(name: settings.name),
- );
-
case DesktopSupportView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
diff --git a/lib/services/shopinbit/shopinbit_service.dart b/lib/services/shopinbit/shopinbit_service.dart
index b066943..d8d2cee 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<dynamic>(
- 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<String?> loadCustomerKey() =>
+ _secure.read(key: _kShopinBitCustomerKeyKeySecureStore);
+
Future<String> ensureCustomerKey() async {
- if (_customerKey != null) return _customerKey!;
- _customerKey =
- DB.instance.get<dynamic>(
- 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<dynamic>(
- 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<void> setCustomerKey(String key) async {
- _customerKey = key;
+ await _secure.write(key: _kShopinBitCustomerKeyKeySecureStore, value: key);
client.externalCustomerKey = key;
- await DB.instance.put<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitCustomerKey",
- value: key,
- );
- Logging.instance.i("ShopInBitService: customer key manually set");
+ Logging.instance.i("ShopInBitService: customer key stored");
}
Future<void> clearCustomerKey() async {
- _customerKey = null;
client.externalCustomerKey = null;
- await DB.instance.put<dynamic>(
- 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<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitGuidelinesAccepted",
- )
- as bool? ??
- false;
- return _guidelinesAccepted!;
- }
-
- Future<void> setGuidelinesAccepted(bool accepted) async {
- _guidelinesAccepted = accepted;
- await DB.instance.put<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitGuidelinesAccepted",
- value: accepted,
- );
- Logging.instance.i(
- "ShopInBitService: guidelines accepted set to $accepted",
- );
- }
-
- bool loadSetupComplete() {
- if (_setupComplete != null) return _setupComplete!;
- _setupComplete =
- DB.instance.get<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitSetupComplete",
- )
- as bool? ??
- false;
- return _setupComplete!;
- }
-
- Future<void> setSetupComplete(bool complete) async {
- _setupComplete = complete;
- await DB.instance.put<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitSetupComplete",
- value: complete,
- );
- Logging.instance.i("ShopInBitService: setup complete set to $complete");
- }
-
- String? loadDisplayName() {
- if (_displayName != null) return _displayName;
- _displayName =
- DB.instance.get<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitDisplayName",
- )
- as String?;
- return _displayName;
- }
-
- Future<void> setDisplayName(String name) async {
- _displayName = name;
- await DB.instance.put<dynamic>(
- boxName: DB.boxNamePrefs,
- key: "shopInBitDisplayName",
- value: name,
- );
- Logging.instance.i("ShopInBitService: display name set");
- }
}
diff --git a/test/services/paynym/paynym_is_api_test.mocks.dart b/test/services/paynym/paynym_is_api_test.mocks.dart
index e3d6837..c62d8cc 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<String, String>? 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<String, String>? 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>);
}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.