From 58659e8f7f20d352064d3b21b1d14e8e57a8f126 Mon Sep 17 00:00:00 2001 From: bponders12-cell Date: Tue, 1 Sep 2026 23:12:44 -0400 Subject: [PATCH] fix: migrate offline sync outbox into authenticated account --- lib/services/sync/sync_outbox.dart | 48 ++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/services/sync/sync_outbox.dart b/lib/services/sync/sync_outbox.dart index 007e817..37e669f 100644 --- a/lib/services/sync/sync_outbox.dart +++ b/lib/services/sync/sync_outbox.dart @@ -12,12 +12,13 @@ class SyncOutbox { Future _saveChain = Future.value(); bool _isLoaded = false; - /// The namespace must be tied to the current Firebase UID (or a local - /// signed-out namespace). This prevents one account's sync state from being - /// mistaken for another account's state on the same device. + /// The namespace is account-scoped. Pending work recorded before login is + /// migrated from the local namespace into the newly authenticated account + /// so offline rides do not become stranded when the user signs in later. SyncOutbox(this._prefs, {String? storageNamespace}) : _storageNamespace = _normalizeNamespace(storageNamespace) { load(); + if (_storageNamespace != 'local') _migrateLocalPendingItems(); } static String _normalizeNamespace(String? value) { @@ -57,6 +58,43 @@ class SyncOutbox { _isLoaded = true; } + void _migrateLocalPendingItems() { + final localKey = '${_outboxStorageKeyPrefix}_local'; + final rawJson = _prefs.getString(localKey); + if (rawJson == null || rawJson.trim().isEmpty) return; + + var migrated = 0; + try { + final decoded = jsonDecode(rawJson); + if (decoded is List) { + for (final entry in decoded) { + try { + final map = entry is Map ? entry : Map.from(entry as Map); + final item = SyncItem.fromJson(map); + // Local namespace work cannot legitimately be cloud-synced yet; + // keep only pending work and rebind it to the current provider. + if (item.status.isPending) { + _items[item.key] = item.status == SyncStatus.uploading + ? item.copyWith(status: SyncStatus.queued, clearNextRetry: true) + : item; + migrated++; + } + } catch (itemError) { + debugPrint('[SyncOutbox] Skipped local migration item: $itemError'); + } + } + } + } catch (e) { + debugPrint('[SyncOutbox] Local outbox migration skipped: $e'); + return; + } + + if (migrated > 0) { + _prefs.setString(storageKey, jsonEncode(_items.values.map((e) => e.toJson()).toList())); + _prefs.remove(localKey); + } + } + Future _save() { final operation = _saveChain.then((_) async { if (_items.length > maxOutboxCapacity) { @@ -88,9 +126,7 @@ class SyncOutbox { final key = '$normalizedProvider:$localRideId'; final existing = _items[key]; if (existing != null) { - if (existing.status == SyncStatus.synced || existing.status == SyncStatus.queued || existing.status == SyncStatus.uploading) { - return existing; - } + if (existing.status == SyncStatus.synced || existing.status == SyncStatus.queued || existing.status == SyncStatus.uploading) return existing; final rearmed = existing.copyWith(status: SyncStatus.queued, clearLastError: true, clearNextRetry: true); _items[key] = rearmed; await _save();