Ad and purchases enabled (still need actual testing)

This commit is contained in:
Courtney Arnold 2026-08-16 18:27:56 -05:00
parent 2fcc6dcab7
commit faf319322a
35 changed files with 1676 additions and 34 deletions

View file

@ -70,9 +70,11 @@ class CloudSyncService {
_lastKnownRemoteVersionTag = null;
}
/// Finds-or-creates the `Show Me The Fuel Refund` folder under [parentId]
/// (a folder the user picked in the folder browser) and configures this
/// service to use it. Returns the resulting folder ID.
/// Points this service at the `Show Me The Fuel Refund` folder under
/// [parentId] (a folder the user picked in the folder browser, or —
/// see [AppState.connectProvider] — the assumed root location a fresh
/// connection defaults to) and configures this service to use it.
/// Returns the resulting folder ID.
///
/// If the user picked a folder that's *already* named
/// `Show Me The Fuel Refund` — [currentFolderName] carries that name up
@ -82,12 +84,45 @@ class CloudSyncService {
/// setup, but re-picking that same folder later (e.g. after reinstalling)
/// would double it up as `/Show Me The Fuel Refund/Show Me The Fuel
/// Refund`.
///
/// Otherwise, three cases:
/// - A same-named folder already exists directly under [parentId] (e.g.
/// one someone else already set up there to share) — adopt it as-is,
/// the same convergence behavior as before, so two people pointed at
/// the same shared parent end up sharing one app folder either way.
/// - This service already has an app folder configured somewhere else
/// (from a previous [selectAppFolder] call, or the root default a
/// fresh connection starts with) and [parentId] has no folder of its
/// own yet — *move* the existing one there via
/// [CloudStorageSession.moveFolder], preserving its contents, rather
/// than creating a fresh empty folder and abandoning the old one with
/// all its synced data still in it.
/// - Neither of the above (nothing configured yet, nothing at the
/// destination) — create a fresh one, same as always.
Future<String> selectAppFolder(String parentId, {String? currentFolderName}) async {
final session = provider.beginSession();
try {
final folderId = currentFolderName == appFolderName
? parentId
: await session.findOrCreateFolder(parentId: parentId, name: appFolderName);
if (currentFolderName == appFolderName) {
configure(parentId);
return parentId;
}
CloudFolder? existingAtDestination;
for (final folder in await session.listFolders(parentId: parentId)) {
if (folder.name == appFolderName) {
existingAtDestination = folder;
break;
}
}
final String folderId;
if (existingAtDestination != null) {
folderId = existingAtDestination.id;
} else if (_appFolderId != null) {
folderId = await session.moveFolder(folderId: _appFolderId!, newParentId: parentId);
} else {
folderId = await session.findOrCreateFolder(parentId: parentId, name: appFolderName);
}
configure(folderId);
return folderId;
} finally {
@ -106,9 +141,17 @@ class CloudSyncService {
/// treats it as abandoned (e.g. that device crashed or went offline
/// mid-sync) and deletes it rather than waiting forever. Defaults to 10
/// minutes, matching [defaultStaleLockMinutes] in app_state.dart.
///
/// [beforeUpload], if given, is awaited once — after any pull/merge of
/// remote changes has finished, but before anything local gets uploaded
/// (pending receipt photos or the database snapshot itself). This is the
/// one hook [AppState] uses to show the app's single ad placement, since
/// it's meant to run before *pushing* local data up, not before *pulling*
/// remote data down.
Future<SyncResult> syncNow({
bool keepLocalReceiptCopies = false,
Duration staleLockAge = const Duration(minutes: 10),
Future<void> Function()? beforeUpload,
}) async {
final appFolderId = _appFolderId;
if (!provider.isSignedIn || appFolderId == null) {
@ -137,6 +180,10 @@ class CloudSyncService {
await _pullAndMerge(session, remoteInfo.id);
}
if (beforeUpload != null) {
await beforeUpload();
}
final allReceiptsUploaded =
await _uploadPendingReceipts(session, keepLocalCopies: keepLocalReceiptCopies);
@ -178,6 +225,21 @@ class CloudSyncService {
try {
await db.execute(mergeVehiclesSql);
await db.execute(mergeFuelEntriesSql);
// ad_free_entitlement is a newer table than vehicles/fuel_entries —
// unlike those two (present since the very first schema version, so
// any remote snapshot ever pushed already has them), a remote
// snapshot pushed before this table existed genuinely won't have
// it. Merging against it unconditionally would throw ("no such
// table") on exactly that snapshot; skipping when absent just means
// "that older snapshot has no entitlement info to contribute",
// which is correct — local's own value (if any) is left as-is.
final remoteHasEntitlementTable = (await db.rawQuery(
"SELECT 1 FROM remote_db.sqlite_master WHERE type = 'table' AND name = 'ad_free_entitlement'",
)).isNotEmpty;
if (remoteHasEntitlementTable) {
await db.execute(mergeAdFreeEntitlementSql);
}
} finally {
try {
await db.execute('DETACH DATABASE remote_db');