Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
48517de
fix(shopinbit): escape non-ASCII in request bodies
sneurlax May 26, 2026
e230633
feat(shopinbit): migrate to PUT /payment for 1.0.4
sneurlax May 26, 2026
0f51d51
fix(shopinbit): GET payment first, PUT only if no live invoice
sneurlax May 26, 2026
48bfd1a
chore: Log errors
julian-CStack May 27, 2026
571fc32
fix(ui): mobile button height/size
julian-CStack May 27, 2026
726c21d
fix(ui): fix keyboard covering textfield/dialog on mobile
julian-CStack May 27, 2026
e915b02
fix(ui): more navigation and layout/styling cleanup
julian-CStack May 27, 2026
0b45b0b
Merge branch 'josh/fixes' into julian/fixes
julian-CStack May 27, 2026
4c5680f
fix(ui): shopinbit ticket detail review offer options styling
julian-CStack May 27, 2026
2de3346
fix(ui): this should have been done a long time ago
julian-CStack May 27, 2026
98c4dd9
fix(ui): small nav fix
julian-CStack May 27, 2026
adcbf65
refactor: extract shared payment flow
sneurlax May 27, 2026
738dc1e
fix: use CopyIcon
sneurlax May 27, 2026
c2bd570
fix: guard against non-ETH TRON addresses
sneurlax May 27, 2026
cd2a1b8
fix: use more SW-standard icons
sneurlax May 27, 2026
574392a
refactor(shopinbit): await send-from navigation before returning true
sneurlax May 27, 2026
fc57247
fix(ui): pre-load ShopInBit payment info instead of in-page spinner o…
sneurlax May 27, 2026
b4cb894
fix(shopinbit): don't pop the whole nav stack when PAY NOW has no add…
sneurlax May 27, 2026
9f558ea
fix(shopinbit): keep delivery country consistent in shipping view
sneurlax May 27, 2026
1659182
fix(shopinbit): render locked country as disabled text field
sneurlax May 27, 2026
cf0b443
fix(shopinbit): show payment-check API errors as a blocking dialog
sneurlax May 28, 2026
e691f22
fix(shopinbit): show car research payment processing errors as a dialog
sneurlax May 28, 2026
d1e0a72
fix(shopinbit): show car research request retry errors as a dialog
sneurlax May 28, 2026
0868313
fix(shopinbit): show car research invoice errors as a dialog
sneurlax May 28, 2026
c3e5340
fix(shopinbit): show customer key generation errors as a dialog
sneurlax May 28, 2026
bc567af
fix(shopinbit): show manual customer key set errors as a dialog
sneurlax May 28, 2026
05d6f82
fix(shopinbit): show ticket retry request errors as a dialog
sneurlax May 28, 2026
c15dae4
fix(shopinbit): show step 4 submit errors as a dialog
sneurlax May 28, 2026
48de9d0
fix(cakepay): show missing-payment-data errors as a dialog
sneurlax May 28, 2026
13144c2
chore(shopinbit): drop unused show_flush_bar import from car fee view
sneurlax May 28, 2026
9335dd5
fix(shopinbit): require a live invoice before opening the payment view
sneurlax May 28, 2026
bdb6f7a
feat(shopinbit): add car request payload and invoice recovery to client
sneurlax May 29, 2026
fb4952d
feat(shopinbit): cache car request payload when creating the fee invoice
sneurlax May 29, 2026
8981054
refactor(shopinbit): finalize car research via backend failsafe
sneurlax May 29, 2026
992d17e
feat(shopinbit): resume car research from server-side current invoices
sneurlax May 29, 2026
1c503d9
refactor(shopinbit): retire manual car research request retry
sneurlax May 29, 2026
2d5c5b4
fix(desktop settings): clamp selected menu index to prevent RangeError
sneurlax May 29, 2026
28cc575
refactor(shopinbit): resume car research with inline row spinner
sneurlax May 30, 2026
0132c18
Revert "fix(desktop settings): clamp selected menu index to prevent R…
julian-CStack May 30, 2026
cfb37fe
pre loading example combined with required args in widget/view
julian-CStack May 28, 2026
505d15d
Merge remote-tracking branch 'origin/staging' into julian/testing2
julian-CStack May 31, 2026
0042ca9
re enable shopinbit
julian-CStack May 31, 2026
1a804a5
shopinbit refactor wip
julian-CStack Jun 1, 2026
44042b0
fix cakepay order refresh so awaiters can be sure a refresh has occurred
julian-CStack Jun 1, 2026
4ef3fb3
fix: record order by using named params
julian-CStack Jun 1, 2026
44531dd
fix: log polling issue. Dialog isn't great here as its polling and...…
julian-CStack Jun 1, 2026
170cd9d
fix: empty string in response and more logging
julian-CStack Jun 1, 2026
c25b5cb
fix: optimize a little bit
julian-CStack Jun 1, 2026
1f42db5
fix: add terminal states
julian-CStack Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 176 additions & 28 deletions lib/db/drift/shared_db/shared_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path/path.dart' as path;

import '../../../models/shopinbit/shopinbit_order_model.dart'
show ShopInBitCategory, ShopInBitOrderStatus;
import "../../../models/shopinbit/shopinbit_enums.dart";
import "../../../services/shopinbit/src/models/message.dart";
import '../../../utilities/stack_file_system.dart';
import 'tables/cakepay_orders.dart';
import 'tables/shopin_bit_settings.dart';
Expand All @@ -27,8 +27,8 @@ abstract final class SharedDrift {
}

@DriftDatabase(
tables: [CakepayOrders, ShopinBitSettings, ShopInBitTickets],
daos: [ShopinBitSettingsDao],
tables: [CakepayOrders, ShopInBitSettings, ShopInBitTickets],
daos: [ShopInBitSettingsDao, ShopInBitTicketsDao],
)
final class SharedDatabase extends _$SharedDatabase {
SharedDatabase._([QueryExecutor? executor])
Expand All @@ -41,7 +41,7 @@ final class SharedDatabase extends _$SharedDatabase {
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
if (from == 1 && to == 2) {
await m.createTable(shopinBitSettings);
await m.createTable(shopInBitSettings);
await m.createTable(shopInBitTickets);
}
},
Expand All @@ -61,35 +61,183 @@ final class SharedDatabase extends _$SharedDatabase {
}
}

@DriftAccessor(tables: [ShopinBitSettings])
class ShopinBitSettingsDao extends DatabaseAccessor<SharedDatabase>
with _$ShopinBitSettingsDaoMixin {
ShopinBitSettingsDao(super.db);
@DriftAccessor(tables: [ShopInBitTickets])
class ShopInBitTicketsDao extends DatabaseAccessor<SharedDatabase>
with _$ShopInBitTicketsDaoMixin {
ShopInBitTicketsDao(super.db);

Future<ShopinBitSetting> getSettings() async {
final ShopinBitSetting? row = await (select(
shopinBitSettings,
)..where((t) => t.id.equals(0))).getSingleOrNull();
if (row != null) return row;
// -- Reads --

return into(
shopinBitSettings,
).insertReturning(ShopinBitSettingsCompanion.insert(id: const Value(0)));
Future<ShopInBitTicket?> getByApiId(int apiTicketId) {
return (select(
shopInBitTickets,
)..where((t) => t.apiTicketId.equals(apiTicketId))).getSingleOrNull();
}

Future<void> setGuidelinesAccepted(bool accepted) =>
_update(ShopinBitSettingsCompanion(guidelinesAccepted: Value(accepted)));
Stream<ShopInBitTicket?> watchByApiId(int apiTicketId) {
return (select(
shopInBitTickets,
)..where((t) => t.apiTicketId.equals(apiTicketId))).watchSingleOrNull();
}

Future<List<ShopInBitTicket>> getByCustomerKey(String customerKey) {
return (select(shopInBitTickets)
..where((t) => t.customerKey.equals(customerKey))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.get();
}

/// All tickets for the active customer key, newest first.
Stream<List<ShopInBitTicket>> watchByCustomerKey(String customerKey) {
return (select(shopInBitTickets)
..where((t) => t.customerKey.equals(customerKey))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}

// -- Writes --

/// Insert a brand-new ticket. Caller must supply every required field;
/// pass nullable fields through the companion's `Value(...)` wrappers.
Future<void> insertTicket(ShopInBitTicketsCompanion companion) async {
await into(shopInBitTickets).insert(companion);
}

/// Patch an existing ticket. Use `Value.absent()` (the companion default)
/// for fields you don't want to touch. Returns true if a row was updated.
Future<bool> updateTicket(
int apiTicketId,
ShopInBitTicketsCompanion patch,
) async {
final int rows = await (update(
shopInBitTickets,
)..where((t) => t.apiTicketId.equals(apiTicketId))).write(patch);
return rows > 0;
}

Future<int> deleteByApiId(int apiTicketId) {
return (delete(
shopInBitTickets,
)..where((t) => t.apiTicketId.equals(apiTicketId))).go();
}

Future<int> deleteByCustomerKey(String customerKey) {
return (delete(
shopInBitTickets,
)..where((t) => t.customerKey.equals(customerKey))).go();
}
}

@DriftAccessor(tables: [ShopInBitSettings])
class ShopInBitSettingsDao extends DatabaseAccessor<SharedDatabase>
with _$ShopInBitSettingsDaoMixin {
ShopInBitSettingsDao(super.db);

// -- "Current" (= most-recently-used) row --

/// Returns the settings row for the most-recently-used customer key,
/// or null if the user has never generated/recovered one.
Future<ShopInBitSetting?> getCurrentSettings() {
return (select(shopInBitSettings)
..orderBy([(t) => OrderingTerm.desc(t.lastUsedAt)])
..limit(1))
.getSingleOrNull();
}

Stream<ShopInBitSetting?> watchCurrentSettings() {
return (select(shopInBitSettings)
..orderBy([(t) => OrderingTerm.desc(t.lastUsedAt)])
..limit(1))
.watchSingleOrNull();
}

// -- Specific row by customer key --

Future<ShopInBitSetting?> getByKey(String customerKey) {
return (select(
shopInBitSettings,
)..where((t) => t.customerKey.equals(customerKey))).getSingleOrNull();
}

Stream<ShopInBitSetting?> watchByKey(String customerKey) {
return (select(
shopInBitSettings,
)..where((t) => t.customerKey.equals(customerKey))).watchSingleOrNull();
}

Future<void> setSetupComplete(bool complete) =>
_update(ShopinBitSettingsCompanion(setupComplete: Value(complete)));
Stream<List<ShopInBitSetting>> watchAll() {
return (select(
shopInBitSettings,
)..orderBy([(t) => OrderingTerm.desc(t.lastUsedAt)])).watch();
}

Future<void> setDisplayName(String name) =>
_update(ShopinBitSettingsCompanion(displayName: Value(name)));
// -- Writes --

Future<void> _update(ShopinBitSettingsCompanion changes) async {
await getSettings(); // ensure row exists
await (update(
shopinBitSettings,
)..where((t) => t.id.equals(0))).write(changes);
/// Insert if missing, otherwise bump [lastUsedAt]. Returns the row.
Future<ShopInBitSetting> upsert(String customerKey) {
final DateTime now = DateTime.now();
return into(shopInBitSettings).insertReturning(
ShopInBitSettingsCompanion.insert(
customerKey: customerKey,
createdAt: Value(now),
lastUsedAt: Value(now),
),
onConflict: DoUpdate(
(_) => ShopInBitSettingsCompanion(lastUsedAt: Value(now)),
target: [shopInBitSettings.customerKey],
),
);
}

Future<int> touch(String customerKey) => _write(
customerKey,
ShopInBitSettingsCompanion(lastUsedAt: Value(DateTime.now())),
);

Future<int> setPrivacyAccepted(String customerKey, bool value) => _write(
customerKey,
ShopInBitSettingsCompanion(privacyAccepted: Value(value)),
);

Future<int> setGuidelinesAccepted(
String customerKey,
ShopInBitCategory category,
bool value,
) {
final ShopInBitSettingsCompanion patch = switch (category) {
.concierge => ShopInBitSettingsCompanion(
conciergeGuidelinesAccepted: Value(value),
),
.travel => ShopInBitSettingsCompanion(
travelGuidelinesAccepted: Value(value),
),
.car => ShopInBitSettingsCompanion(carGuidelinesAccepted: Value(value)),
};
return _write(customerKey, patch);
}

Future<int> setSetupComplete(String customerKey, bool value) => _write(
customerKey,
ShopInBitSettingsCompanion(setupComplete: Value(value)),
);

Future<int> deleteByKey(String customerKey) {
return (delete(
shopInBitSettings,
)..where((t) => t.customerKey.equals(customerKey))).go();
}

Future<int> _write(String customerKey, ShopInBitSettingsCompanion changes) {
return (update(
shopInBitSettings,
)..where((t) => t.customerKey.equals(customerKey))).write(changes);
}
}

extension ShopInBitSettingGuidelines on ShopInBitSetting {
bool guidelinesAcceptedFor(ShopInBitCategory category) => switch (category) {
.concierge => conciergeGuidelinesAccepted,
.travel => travelGuidelinesAccepted,
.car => carGuidelinesAccepted,
};
}
Loading
Loading