Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 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
a5b8a4b
fix(shopinbit): open the real car ticket after the research fee, not …
sneurlax Jun 1, 2026
5de2257
fix(shopinbit): tolerate empty/missing fields when parsing API JSON
sneurlax Jun 2, 2026
7ba661a
fix(cakepay): make refreshAll single-flight so awaiters see completion
sneurlax Jun 2, 2026
f6babb4
fix(shopinbit): stop polling a ticket once it reaches a terminal state
sneurlax Jun 2, 2026
d911bfd
fix: log previously-swallowed errors in ShopinBit and CakePay flows
sneurlax Jun 2, 2026
a10633a
fix(shopinbit): retry the real car ticket longer, offer a My Requests…
sneurlax Jun 2, 2026
0a387bd
fix(shopinbit): don't tear down the payment dialog when opening Send …
sneurlax Jun 2, 2026
2b4d0cb
feat(shopinbit): show a QR code for manual crypto payments
sneurlax Jun 2, 2026
5caf409
fix(shopinbit): back off the real-car-ticket adoption retries
sneurlax Jun 4, 2026
5ebb52c
fix(shopinbit): back off pollers on error and pause when backgrounded
sneurlax Jun 4, 2026
60559c4
fix(shopinbit): combine by-customer and car-invoice fetches
sneurlax Jun 4, 2026
25541dd
feat(shopinbit): after payment, nav back to specific request if known
sneurlax Jun 4, 2026
9934533
fix(shopinbit): retry 429s with backoff at the request chokepoint
sneurlax Jun 4, 2026
fb6ea0c
fix(shopinbit): migrate car research flow to API v1.0.6
sneurlax Jun 9, 2026
090ab33
refactor(shopinbit): consolidate poll backoff into the client
sneurlax Jun 10, 2026
77461f1
fix(shopinbit): drop the by-customer car ticket fallback
sneurlax Jun 10, 2026
f01dc8c
refactor(shopinbit): drop the single-flight ticket/invoice fetch wrap…
sneurlax Jun 10, 2026
b9b104f
fix: race condition when refreshing all shopinbit tickets when not al…
julian-CStack Jun 10, 2026
d1457a0
fix(shopinbit): regenerate expired invoice via PUT ?retry=true
sneurlax Jun 10, 2026
85e8b39
fix(shopinbit): re-authenticate once on HTTP 401
sneurlax Jun 10, 2026
763a241
Merge origin/julian/testing into josh/fixes
sneurlax Jun 10, 2026
716c6e3
chore(shopinbit): clean up merge lints
sneurlax Jun 10, 2026
c5f2001
fix(shopinbit): recover car-research invoices within the +24h grace
sneurlax Jun 10, 2026
0d54cd9
fix(shopinbit): treat an empty 2xx body as an error
sneurlax Jun 10, 2026
1e820fd
fix(shopinbit): keep car-research expiresAt null on parse failure
sneurlax Jun 10, 2026
860bd1a
fix(shopinbit): surface parse errors for required ticket fields
sneurlax Jun 10, 2026
8e33585
fix(shopinbit): require remaining required fields across models
sneurlax Jun 10, 2026
e61d840
chore(shopinbit): address review on car-research finalize
sneurlax Jun 11, 2026
0119c48
fix(shopinbit): handle no_payment_required as fully covered
sneurlax Jun 11, 2026
6b7d9bc
fix(shopinbit): only mark car research complete once the ticket exists
sneurlax Jun 11, 2026
adc937f
docs(cakepay): drop misleading comment on refreshAll ignore
sneurlax Jun 11, 2026
65542e9
fix(cakepay): log refreshAll errors without propagating them
sneurlax Jun 11, 2026
a201981
fix(cakepay): show a flushbar when pull-to-refresh fails
sneurlax Jun 11, 2026
496d998
fix: always show/log error in orders view and ensure its propagated f…
julian-CStack Jun 11, 2026
d541d20
fix(shopinbit): remove pointless mounted check and verbose comments
sneurlax Jun 11, 2026
2f8328e
fix(shopinbit): remove unused customerKey field from ApiResponse
sneurlax Jun 11, 2026
d3aa32e
chore: ensure expected field parsing fails ungracefully
julian-CStack Jun 11, 2026
a1ccc0c
fix: ensure ticket gets stored on car invoice polling ticket created/…
julian-CStack Jun 11, 2026
7dbea23
fix: API was updated to show ticket type so we can now filter receipt…
julian-CStack Jun 11, 2026
a8e74ff
fix: reduce pointless API calls
julian-CStack Jun 11, 2026
aae436b
Merge remote-tracking branch 'origin/staging' into julian/testing
julian-CStack Jun 12, 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
197 changes: 169 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,176 @@ 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();
}

/// 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();
}

Future<void> setSetupComplete(bool complete) =>
_update(ShopinBitSettingsCompanion(setupComplete: Value(complete)));
Stream<ShopInBitSetting?> watchByKey(String customerKey) {
return (select(
shopInBitSettings,
)..where((t) => t.customerKey.equals(customerKey))).watchSingleOrNull();
}

Stream<List<ShopInBitSetting>> watchAll() {
return (select(
shopInBitSettings,
)..orderBy([(t) => OrderingTerm.desc(t.lastUsedAt)])).watch();
}

// -- Writes --

/// 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<void> setDisplayName(String name) =>
_update(ShopinBitSettingsCompanion(displayName: Value(name)));
Future<int> setPrivacyAccepted(String customerKey, bool value) => _write(
customerKey,
ShopInBitSettingsCompanion(privacyAccepted: Value(value)),
);

Future<void> _update(ShopinBitSettingsCompanion changes) async {
await getSettings(); // ensure row exists
await (update(
shopinBitSettings,
)..where((t) => t.id.equals(0))).write(changes);
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