diff --git a/lib/services/github/github_sync_service.dart b/lib/services/github/github_sync_service.dart index e40c3af..47c5ec9 100644 --- a/lib/services/github/github_sync_service.dart +++ b/lib/services/github/github_sync_service.dart @@ -34,16 +34,16 @@ class GitHubUploadResult { }) : timestamp = timestamp ?? DateTime.now(); } -/// Ride synchronization service. +/// Compatibility facade for the historical ride-detail sync service. /// -/// Historical versions of the ride-detail screen called this service directly, -/// which required a separate GitHub PAT even when the user was already signed -/// into MotorCloud with Google/Firebase. When no GitHub PAT is configured we now -/// use the authoritative Firebase + R2 provider, matching Settings > Back Up All. -/// GitHub remains available when a PAT is explicitly configured. +/// MotorSignal cloud backup is Firebase Authentication + Firestore + R2. +/// The old GitHub PAT exporter remains available only through the explicit +/// `uploadRideToGitHub` developer/export path and is never used by the normal +/// History -> Upload Now action. class GitHubSyncService extends ChangeNotifier { final AppDatabase _db; - final GitHubAuthService _auth; + final GitHubAuthService _githubAuth; + final AuthService _authService; final SharedPreferences _prefs; final http.Client _client; @@ -51,16 +51,40 @@ class GitHubSyncService extends ChangeNotifier { String _statusMessage = ''; final Set _uploadQueue = {}; - GitHubSyncService(this._db, this._auth, this._prefs, [http.Client? client]) - : _client = client ?? http.Client(); + GitHubSyncService( + this._db, + this._githubAuth, + this._authService, + this._prefs, [http.Client? client], + ) : _client = client ?? http.Client(); GitHubUploadState get state => _state; String get statusMessage => _statusMessage; Set get queuedRideIds => Set.unmodifiable(_uploadQueue); - bool isRideUploaded(int rideId) => _prefs.getBool('github_uploaded_ride_$rideId') ?? false; - String? getRideCommitSha(int rideId) => _prefs.getString('github_sha_ride_$rideId'); - String? getRideUploadTimestamp(int rideId) => _prefs.getString('github_uploaded_at_ride_$rideId'); + String? _userScopedKey(String key, int rideId) { + final uid = _authService.currentUser?.uid; + if (uid == null || uid.isEmpty) return null; + return '${key}_${uid}_$rideId'; + } + + bool isRideUploaded(int rideId) { + final scoped = _userScopedKey('motorcloud_uploaded_ride', rideId); + if (scoped == null) return false; + return _prefs.getBool(scoped) ?? false; + } + + String? getRideCommitSha(int rideId) { + final scoped = _userScopedKey('motorcloud_ride_remote_id', rideId); + if (scoped == null) return null; + return _prefs.getString(scoped); + } + + String? getRideUploadTimestamp(int rideId) { + final scoped = _userScopedKey('motorcloud_uploaded_at_ride', rideId); + if (scoped == null) return null; + return _prefs.getString(scoped); + } void queueRideForUpload(int rideId) { if (isRideUploaded(rideId)) return; @@ -79,19 +103,43 @@ class GitHubSyncService extends ChangeNotifier { } } + /// Normal History -> Upload Now operation. + /// + /// This deliberately ignores any configured GitHub PAT. A user's ride must + /// be written to that user's MotorCloud account, not to a public developer + /// repository. Authentication and ownership are enforced by Firebase/R2. Future uploadRide(int rideId, {bool isManual = false}) async { - // IMPORTANT: Google/Firebase authentication is the normal MotorCloud cloud - // authentication. A GitHub PAT is optional and only needed for explicit - // GitHub repository export. - final githubToken = _auth.getToken(); - if (githubToken == null || githubToken.isEmpty) { - return _uploadToMotorCloud(rideId); - } + return _uploadToMotorCloud(rideId); + } + /// Explicit legacy/developer export. Not used by the normal ride UI. + Future uploadRideToGitHub(int rideId) async { + final githubToken = _githubAuth.getToken(); + if (githubToken == null || githubToken.isEmpty) { + return GitHubUploadResult( + success: false, + message: 'GitHub export requires an explicitly configured GitHub token.', + ); + } return _uploadToGitHub(rideId, githubToken); } Future _uploadToMotorCloud(int rideId) async { + final user = _authService.currentUser; + if (user == null || user.isAnonymous) { + _state = GitHubUploadState.authRequired; + _statusMessage = 'Sign in to MotorCloud before uploading a ride.'; + notifyListeners(); + return GitHubUploadResult(success: false, message: _statusMessage); + } + + if (isRideUploaded(rideId)) { + _state = GitHubUploadState.success; + _statusMessage = 'Ride #$rideId is already backed up to your MotorCloud account.'; + notifyListeners(); + return GitHubUploadResult(success: true, message: _statusMessage, commitSha: getRideCommitSha(rideId), targetPath: 'MotorCloud'); + } + _state = GitHubUploadState.uploading; _statusMessage = 'Backing up Ride #$rideId to MotorCloud...'; notifyListeners(); @@ -99,7 +147,7 @@ class GitHubSyncService extends ChangeNotifier { try { final provider = FirebaseSyncProvider( db: _db, - authService: FirebaseAuthService(), + authService: _authService, ); final result = await provider.uploadRide( SyncItem( @@ -110,14 +158,15 @@ class GitHubSyncService extends ChangeNotifier { ); if (result.success) { - await _prefs.setBool('github_uploaded_ride_$rideId', true); - await _prefs.setString('github_uploaded_at_ride_$rideId', DateTime.now().toIso8601String()); - if (result.remoteId != null) { - await _prefs.setString('github_sha_ride_$rideId', result.remoteId!); - } + final uploadedKey = _userScopedKey('motorcloud_uploaded_ride', rideId)!; + final timestampKey = _userScopedKey('motorcloud_uploaded_at_ride', rideId)!; + final remoteIdKey = _userScopedKey('motorcloud_ride_remote_id', rideId)!; + await _prefs.setBool(uploadedKey, true); + await _prefs.setString(timestampKey, DateTime.now().toIso8601String()); + if (result.remoteId != null) await _prefs.setString(remoteIdKey, result.remoteId!); _uploadQueue.remove(rideId); _state = GitHubUploadState.success; - _statusMessage = 'Ride #$rideId backed up to MotorCloud'; + _statusMessage = 'Ride #$rideId backed up to your MotorCloud account'; notifyListeners(); return GitHubUploadResult( success: true, @@ -127,11 +176,7 @@ class GitHubSyncService extends ChangeNotifier { ); } - if (result.requiresAuth) { - _state = GitHubUploadState.authRequired; - } else { - _state = GitHubUploadState.failed; - } + _state = result.requiresAuth ? GitHubUploadState.authRequired : GitHubUploadState.failed; _statusMessage = result.errorMessage ?? 'MotorCloud backup failed'; notifyListeners(); return GitHubUploadResult(success: false, message: _statusMessage); @@ -154,8 +199,8 @@ class GitHubSyncService extends ChangeNotifier { final dateStr = DateFormat('yyyy-MM-dd-HHmmss').format(ride.startTime.toLocal()); final folderName = 'field_test_session_${rideId}_$dateStr/field_test_session'; - final repo = _auth.getRepository(); - final branch = _auth.getBranch(); + final repo = _githubAuth.getRepository(); + final branch = _githubAuth.getBranch(); final fieldTestExport = FieldTestExportService(_db); final exportService = ExportService(_db); final sessionJson = await fieldTestExport.generateFieldTestSessionJson(rideId); @@ -262,17 +307,13 @@ class GitHubSyncService extends ChangeNotifier { } } - await _prefs.setBool('github_uploaded_ride_$rideId', true); - await _prefs.setString('github_uploaded_at_ride_$rideId', DateTime.now().toIso8601String()); - if (lastCommitSha != null) await _prefs.setString('github_sha_ride_$rideId', lastCommitSha); - _uploadQueue.remove(rideId); _state = GitHubUploadState.success; - _statusMessage = 'Ride #$rideId successfully uploaded to $repo ($branch)'; + _statusMessage = 'Ride #$rideId exported to $repo ($branch)'; notifyListeners(); return GitHubUploadResult(success: true, message: _statusMessage, commitSha: lastCommitSha, targetPath: folderName); } catch (e) { _state = GitHubUploadState.failed; - _statusMessage = 'Upload failed for Ride #$rideId: $e'; + _statusMessage = 'GitHub export failed for Ride #$rideId: $e'; notifyListeners(); return GitHubUploadResult(success: false, message: _statusMessage); } @@ -281,7 +322,8 @@ class GitHubSyncService extends ChangeNotifier { final githubSyncServiceProvider = ChangeNotifierProvider((ref) { final db = ref.watch(databaseProvider); - final auth = ref.watch(githubAuthServiceProvider); + final githubAuth = ref.watch(githubAuthServiceProvider); + final authService = ref.watch(authServiceProvider); final prefs = ref.watch(sharedPreferencesProvider); - return GitHubSyncService(db, auth, prefs); -}); \ No newline at end of file + return GitHubSyncService(db, githubAuth, authService, prefs); +});