import 'dart:async'; import 'dart:io' show Platform; import 'package:google_sign_in/google_sign_in.dart'; import 'package:http/http.dart' as http; import 'drive_oauth_config.dart'; /// Full Drive access is required (not the narrower `drive.file` scope) /// because users need to browse to and reuse folders that someone else /// created and shared with them, not just folders/files this app itself /// created. See the plan doc for the tradeoffs (this requires Google /// Cloud Console "Testing" mode with explicit test users, to avoid needing /// a full OAuth verification review). const driveScopes = ['https://www.googleapis.com/auth/drive']; /// Thrown when a Drive API call needs authorization that isn't currently /// available without prompting the user, e.g. during a background sync. class DriveNotAuthorizedException implements Exception { @override String toString() => 'Drive access is not currently authorized.'; } /// Wraps `google_sign_in` for authenticating with Google and producing an /// authenticated [http.Client] for the Drive API. class DriveAuthService { bool _initialized = false; GoogleSignInAccount? _account; bool get isSignedIn => _account != null; String? get currentAccountEmail => _account?.email; Future _ensureInitialized() async { if (_initialized) return; await GoogleSignIn.instance.initialize( clientId: Platform.isIOS ? DriveOAuthConfig.iosClientId : null, serverClientId: Platform.isAndroid ? DriveOAuthConfig.androidServerClientId : null, ); _initialized = true; } /// Attempts to restore a previous sign-in without any UI. Returns true if /// the user is signed in and Drive access is already authorized. Future attemptSilentSignIn() async { await _ensureInitialized(); final account = await GoogleSignIn.instance.attemptLightweightAuthentication(); _account = account; if (account == null) return false; final authorization = await account.authorizationClient.authorizationForScopes(driveScopes); return authorization != null; } /// Interactive sign-in + Drive scope authorization. Must be called from a /// user-initiated action (e.g. a button press). Future signIn() async { await _ensureInitialized(); final account = await GoogleSignIn.instance.authenticate(scopeHint: driveScopes); _account = account; await account.authorizationClient.authorizeScopes(driveScopes); return account.email; } Future signOut() async { await GoogleSignIn.instance.signOut(); _account = null; } /// Builds an [http.Client] that attaches a fresh Drive authorization /// header to every request. Fetches headers per-request (rather than /// once) so a client that lives across a long sync doesn't use a stale, /// expired token. Never prompts for UI — suitable for background sync — /// so throws [DriveNotAuthorizedException] if authorization isn't already /// in place (the caller should treat that as "Drive is disconnected"). http.Client authenticatedHttpClient() { final account = _account; if (account == null) { throw DriveNotAuthorizedException(); } return _DriveHttpClient(account.authorizationClient); } } class _DriveHttpClient extends http.BaseClient { final GoogleSignInAuthorizationClient _authClient; final http.Client _inner = http.Client(); _DriveHttpClient(this._authClient); @override Future send(http.BaseRequest request) async { final headers = await _authClient.authorizationHeaders(driveScopes, promptIfNecessary: false); if (headers == null) { throw DriveNotAuthorizedException(); } request.headers.addAll(headers); return _inner.send(request); } @override void close() { _inner.close(); super.close(); } }