diff --git a/lib/services/sync/firebase_sync_provider.dart b/lib/services/sync/firebase_sync_provider.dart index 5add6e1..0447213 100644 --- a/lib/services/sync/firebase_sync_provider.dart +++ b/lib/services/sync/firebase_sync_provider.dart @@ -37,14 +37,24 @@ class FirebaseSyncProvider implements SyncProvider { } @override - Future requiresAuthentication() async => _authService.currentUser == null; + Future requiresAuthentication() async { + final user = _authService.currentUser; + return user == null || user.isAnonymous; + } @override Future uploadRide(SyncItem item) async { if (!await isAvailable()) return SyncResult.temporaryFailure('Firebase services are unavailable or unconfigured. Ride remains safely stored locally.'); final user = _authService.currentUser; if (user == null || user.isAnonymous) return SyncResult.authRequired(message: 'Sign in to MotorCloud is required for cloud backup.'); - final idToken = await _authService.getIdToken(); + + // Refresh the Firebase ID token before a manual upload. This prevents a + // recently completed sign-in from being rejected because the cached token + // has not yet propagated through the app's auth abstraction. + var idToken = await _authService.getIdToken(); + if (idToken == null || idToken.isEmpty) { + idToken = await _authService.getIdToken(true); + } if (idToken == null || idToken.isEmpty) return SyncResult.authRequired(message: 'Valid Firebase authentication token required for cloud sync.'); try { @@ -59,13 +69,31 @@ class FirebaseSyncProvider implements SyncProvider { if (zipBytes == null || zipBytes.isEmpty) return SyncResult.permanentFailure('Failed generating compressed telemetry bundle for ride #${item.localRideId}.'); final sha256Hash = sha256.convert(zipBytes).toString(); - final r2Result = await _r2UploadService.uploadTelemetryBundle( + var r2Result = await _r2UploadService.uploadTelemetryBundle( idToken: idToken, remoteRideId: remoteRideId, zipBytes: zipBytes, sha256Hash: sha256Hash, appVersion: appIdentity.semanticVersion, ); + + // If the gateway rejects the first token as unauthorized, refresh once + // and retry. This specifically handles token rotation/expiry without + // turning a valid signed-in session into a false authentication error. + if (!r2Result.success && r2Result.requiresAuth) { + final refreshedToken = await _authService.getIdToken(true); + if (refreshedToken != null && refreshedToken.isNotEmpty && refreshedToken != idToken) { + idToken = refreshedToken; + r2Result = await _r2UploadService.uploadTelemetryBundle( + idToken: idToken, + remoteRideId: remoteRideId, + zipBytes: zipBytes, + sha256Hash: sha256Hash, + appVersion: appIdentity.semanticVersion, + ); + } + } + if (!r2Result.success) { if (r2Result.requiresAuth) return SyncResult.authRequired(message: r2Result.errorMessage ?? 'R2 Gateway authentication required'); if (r2Result.isRetryable) return SyncResult.temporaryFailure(r2Result.errorMessage ?? 'Temporary R2 upload error');