@@ -40,7 +40,8 @@ enum AuthProvider { google, apple }
4040// 3. Configure OAuth consent screen
4141//
4242// Note: For Android, clientId is auto-detected from the signing config
43- const String _googleClientIdIOS = '1095513138272-41oj756pperrsh5aqumh3nktvankcdel.apps.googleusercontent.com' ; // iOS OAuth Client ID
43+ const String _googleClientIdIOS =
44+ '1095513138272-41oj756pperrsh5aqumh3nktvankcdel.apps.googleusercontent.com' ; // iOS OAuth Client ID
4445// Web Client ID (used as serverClientId to mint idTokens the issuer
4546// accepts). Single-sourced with the web shell in AuthCore.
4647const String _googleServerClientId = AuthCore .googleWebClientId;
@@ -61,22 +62,22 @@ class AuthUser {
6162 });
6263
6364 Map <String , dynamic > toJson () => {
64- 'id' : id,
65- 'email' : email,
66- 'displayName' : displayName,
67- 'photoUrl' : photoUrl,
68- 'provider' : provider.name,
69- };
65+ 'id' : id,
66+ 'email' : email,
67+ 'displayName' : displayName,
68+ 'photoUrl' : photoUrl,
69+ 'provider' : provider.name,
70+ };
7071
7172 factory AuthUser .fromJson (Map <String , dynamic > json) => AuthUser (
72- id: json['id' ],
73- email: json['email' ],
74- displayName: json['displayName' ],
75- photoUrl: json['photoUrl' ],
76- provider: AuthProvider .values.firstWhere (
77- (e) => e.name == json['provider' ],
78- ),
79- );
73+ id: json['id' ],
74+ email: json['email' ],
75+ displayName: json['displayName' ],
76+ photoUrl: json['photoUrl' ],
77+ provider: AuthProvider .values.firstWhere (
78+ (e) => e.name == json['provider' ],
79+ ),
80+ );
8081}
8182
8283class AuthService {
@@ -124,9 +125,11 @@ class AuthService {
124125 Future <bool > hasStoredCredentials () async {
125126 if (_currentUser != null ) return true ;
126127 try {
127- final userJson = await SecureStorageService .instance.readJson (
128- SecureStorageKeys .userCredentials,
129- ).timeout (const Duration (seconds: 2 ));
128+ final userJson = await SecureStorageService .instance
129+ .readJson (
130+ SecureStorageKeys .userCredentials,
131+ )
132+ .timeout (const Duration (seconds: 2 ));
130133 return userJson != null ;
131134 } catch (e) {
132135 debugPrint ('hasStoredCredentials check failed: $e ' );
@@ -153,14 +156,18 @@ class AuthService {
153156
154157 if (Platform .isAndroid) {
155158 // Android: clientId is auto-detected, only serverClientId needed for idToken
156- serverClientId = _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
159+ serverClientId =
160+ _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
157161 } else if (Platform .isIOS) {
158162 clientId = _googleClientIdIOS.isNotEmpty ? _googleClientIdIOS : null ;
159- serverClientId = _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
163+ serverClientId =
164+ _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
160165 } else {
161166 // Desktop (Windows/macOS/Linux): use server client ID for browser-based OAuth
162- clientId = _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
163- serverClientId = _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
167+ clientId =
168+ _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
169+ serverClientId =
170+ _googleServerClientId.isNotEmpty ? _googleServerClientId : null ;
164171 }
165172
166173 await _googleSignIn.initialize (
@@ -188,13 +195,15 @@ class AuthService {
188195 SecureStorageKeys .userCredentials,
189196 );
190197
191- debugPrint ('AuthService: userJson = ${userJson != null ? "found" : "null" }' );
198+ debugPrint (
199+ 'AuthService: userJson = ${userJson != null ? "found" : "null" }' );
192200
193201 if (userJson != null ) {
194202 _setCurrentUser (AuthUser .fromJson (userJson));
195203 debugPrint ('AuthService: Restored user: ${_currentUser !.email }' );
196204 await _deriveEncryptionKey ();
197- debugPrint ('AuthService: After _deriveEncryptionKey, key is ${_encryptionKey == null ? "null" : "set" }' );
205+ debugPrint (
206+ 'AuthService: After _deriveEncryptionKey, key is ${_encryptionKey == null ? "null" : "set" }' );
198207 await _initializeFulaClient ();
199208 // Re-link cloud mappings and restore tags for reinstall persistence (runs in background)
200209 if (FulaApiService .instance.isConfigured && ! skipHeavyOperations) {
@@ -223,13 +232,15 @@ class AuthService {
223232 Future <AuthUser ?> signInWithGoogle () async {
224233 try {
225234 if (PlatformCapabilities .isDesktop) {
226- throw Exception ('Google Sign-In is not available on desktop. Please use "Get API Key" to connect your account.' );
235+ throw Exception (
236+ 'Google Sign-In is not available on desktop. Please use "Get API Key" to connect your account.' );
227237 }
228238
229239 await _ensureGoogleInitialized ();
230240
231241 if (! _googleSignIn.supportsAuthenticate ()) {
232- debugPrint ('Google Sign-In: authenticate not supported on this platform' );
242+ debugPrint (
243+ 'Google Sign-In: authenticate not supported on this platform' );
233244 throw Exception ('Google Sign-In not supported on this device' );
234245 }
235246
@@ -255,30 +266,43 @@ class AuthService {
255266 debugPrint ('Google Sign-In error: $e ' );
256267 // Check for common Credential Manager errors
257268 final errorStr = e.toString ();
258- if (errorStr.contains ('GetCredentialResponse' ) || errorStr.contains ('CredMan' )) {
259- throw Exception ('Google Sign-In configuration error. Please check SHA-1 fingerprint and OAuth client IDs in Google Cloud Console.' );
269+ if (errorStr.contains ('GetCredentialResponse' ) ||
270+ errorStr.contains ('CredMan' )) {
271+ throw Exception (
272+ 'Google Sign-In configuration error. Please check SHA-1 fingerprint and OAuth client IDs in Google Cloud Console.' );
260273 }
261274 rethrow ;
262275 }
263276 }
264277
265278 Future <bool > requestFormsScope () async {
266- if (PlatformCapabilities .isDesktop || _currentUser? .provider != AuthProvider .google) {
279+ if (PlatformCapabilities .isDesktop ||
280+ _currentUser? .provider != AuthProvider .google) {
281+ return false ;
282+ }
283+ try {
284+ final authz = await _googleSignIn.authorizationClient.authorizeScopes (
285+ ['https://www.googleapis.com/auth/forms.body' ],
286+ );
287+ return authz != null ;
288+ } catch (e) {
289+ debugPrint ('Error requesting forms scope: $e ' );
267290 return false ;
268291 }
269- final granted = await _googleSignIn.requestScopes (['https://www.googleapis.com/auth/forms.body' ]);
270- return granted;
271292 }
272293
273294 Future <String ?> getGoogleAccessToken () async {
274295 if (_currentUser? .provider != AuthProvider .google) return null ;
275-
276- // For google_sign_in v7, currentUser getter on the plugin returns the current GoogleSignInAccount
277- final account = _googleSignIn.currentUser;
278- if (account == null ) return null ;
279-
280- final auth = await account.authentication;
281- return auth.accessToken;
296+
297+ try {
298+ final authz = await _googleSignIn.authorizationClient.authorizationForScopes (
299+ ['https://www.googleapis.com/auth/forms.body' ],
300+ );
301+ return authz? .accessToken;
302+ } catch (e) {
303+ debugPrint ('Error getting google access token: $e ' );
304+ return null ;
305+ }
282306 }
283307
284308 Future <void > _handleGoogleSignIn (
@@ -344,7 +368,8 @@ class AuthService {
344368 ShelfStorageService .instance.restoreFromCloud ();
345369 }
346370 } catch (e) {
347- debugPrint ('Google Sign-In: Fula initialization failed (sign-in still succeeded): $e ' );
371+ debugPrint (
372+ 'Google Sign-In: Fula initialization failed (sign-in still succeeded): $e ' );
348373 // Sign-in succeeded, but Fula features won't work until RustLib is properly initialized
349374 }
350375 }
@@ -502,7 +527,8 @@ class AuthService {
502527 ShelfStorageService .instance.restoreFromCloud ();
503528 }
504529 } catch (e) {
505- debugPrint ('Apple Sign-In: Fula initialization failed (sign-in still succeeded): $e ' );
530+ debugPrint (
531+ 'Apple Sign-In: Fula initialization failed (sign-in still succeeded): $e ' );
506532 // Sign-in succeeded, but Fula features won't work until RustLib is properly initialized
507533 }
508534 }
@@ -620,7 +646,8 @@ class AuthService {
620646 /// Initialize the fula_client with the derived encryption key
621647 Future <void > _initializeFulaClient () async {
622648 debugPrint ('AuthService: _initializeFulaClient called' );
623- debugPrint ('AuthService: _encryptionKey is ${_encryptionKey == null ? "null" : "set (${_encryptionKey !.length } bytes)" }' );
649+ debugPrint (
650+ 'AuthService: _encryptionKey is ${_encryptionKey == null ? "null" : "set (${_encryptionKey !.length } bytes)" }' );
624651
625652 if (_encryptionKey == null ) {
626653 debugPrint ('Cannot initialize FulaApiService: no encryption key' );
@@ -701,12 +728,14 @@ class AuthService {
701728
702729 // If no current user, try to restore the session first
703730 if (_currentUser == null ) {
704- debugPrint ('AuthService: No current user, attempting to restore session...' );
731+ debugPrint (
732+ 'AuthService: No current user, attempting to restore session...' );
705733 final hasSession = await checkExistingSession ();
706734 debugPrint ('AuthService: Session restore result: $hasSession ' );
707735 // checkExistingSession already calls _initializeFulaClient if successful
708736 if (hasSession && FulaApiService .instance.isConfigured) {
709- debugPrint ('AuthService: FulaApiService already initialized via session restore' );
737+ debugPrint (
738+ 'AuthService: FulaApiService already initialized via session restore' );
710739 return ;
711740 }
712741 }
@@ -715,7 +744,8 @@ class AuthService {
715744 if (_encryptionKey == null ) {
716745 debugPrint ('AuthService: No encryption key, calling getEncryptionKey()' );
717746 await getEncryptionKey ();
718- debugPrint ('AuthService: After getEncryptionKey(), _encryptionKey is ${_encryptionKey == null ? "null" : "set" }' );
747+ debugPrint (
748+ 'AuthService: After getEncryptionKey(), _encryptionKey is ${_encryptionKey == null ? "null" : "set" }' );
719749 }
720750 await _initializeFulaClient ();
721751
@@ -741,7 +771,8 @@ class AuthService {
741771 final stored = await SecureStorageService .instance.read (
742772 SecureStorageKeys .encryptionKey,
743773 );
744- debugPrint ('AuthService: Stored encryption key = ${stored != null ? "found" : "null" }' );
774+ debugPrint (
775+ 'AuthService: Stored encryption key = ${stored != null ? "found" : "null" }' );
745776
746777 if (stored != null ) {
747778 _encryptionKey = base64Decode (stored);
@@ -754,7 +785,8 @@ class AuthService {
754785 return _encryptionKey;
755786 }
756787
757- debugPrint ('AuthService: Cannot get encryption key - no stored key and no current user' );
788+ debugPrint (
789+ 'AuthService: Cannot get encryption key - no stored key and no current user' );
758790 return null ;
759791 }
760792
@@ -838,14 +870,15 @@ class AuthService {
838870 /// material — Mode A `oauthSub` , Mode B `oauthSub` (read from
839871 /// SecureStorage), `effectiveUserId` , and `usersIndexUserKey` are
840872 /// all non-confidential.
841- Future <({
842- String mode,
843- String ? provider,
844- String ? oauthSub,
845- String ? effectiveUserId,
846- String ? email,
847- String ? usersIndexUserKey,
848- })?> getDerivationInputs () async {
873+ Future <
874+ ({
875+ String mode,
876+ String ? provider,
877+ String ? oauthSub,
878+ String ? effectiveUserId,
879+ String ? email,
880+ String ? usersIndexUserKey,
881+ })?> getDerivationInputs () async {
849882 if (_currentUser == null ) return null ;
850883
851884 final modeVersion = await SecureStorageService .instance.read (
@@ -866,20 +899,19 @@ class AuthService {
866899 );
867900 final jwtSub = AuthCore .extractJwtSub (jwt);
868901 if (jwtSub != null && jwtSub.isNotEmpty) {
869- usersIndexUserKey =
870- await fula.deriveUserKeyFromJwtSub (jwtSub: jwtSub);
902+ usersIndexUserKey = await fula.deriveUserKeyFromJwtSub (jwtSub: jwtSub);
871903 }
872904 } catch (e) {
873- debugPrint ('AuthService.getDerivationInputs: JWT-sub deriveUserKey failed: $e ' );
905+ debugPrint (
906+ 'AuthService.getDerivationInputs: JWT-sub deriveUserKey failed: $e ' );
874907 }
875908
876909 if (mode == 'A' ) {
877910 final pinned = await SecureStorageService .instance.read (
878911 SecureStorageKeys .derivationEmail,
879912 );
880- final email = (pinned != null && pinned.isNotEmpty)
881- ? pinned
882- : _currentUser! .email;
913+ final email =
914+ (pinned != null && pinned.isNotEmpty) ? pinned : _currentUser! .email;
883915
884916 // Email-keyed fallback only makes sense for Mode A: master keys
885917 // Mode A users by email-hash. Mode B/C key by the seed-derived
@@ -888,7 +920,8 @@ class AuthService {
888920 try {
889921 usersIndexUserKey = await fula.deriveUserKeyFromEmail (email: email);
890922 } catch (e) {
891- debugPrint ('AuthService.getDerivationInputs: email deriveUserKey failed: $e ' );
923+ debugPrint (
924+ 'AuthService.getDerivationInputs: email deriveUserKey failed: $e ' );
892925 usersIndexUserKey = null ;
893926 }
894927 }
@@ -1157,7 +1190,8 @@ class AuthService {
11571190 /// persisted to disk — it's used once for KDF + signing, then dropped.
11581191 ///
11591192 /// On `GoogleSignInExceptionCode.canceled` , returns `null` .
1160- Future <({AuthUser user, bool hasModeA})?> signInGoogleModeB ({required String password}) async {
1193+ Future <({AuthUser user, bool hasModeA})?> signInGoogleModeB (
1194+ {required String password}) async {
11611195 if (PlatformCapabilities .isDesktop) {
11621196 throw Exception (
11631197 'Google Sign-In is not available on desktop. '
@@ -1195,7 +1229,8 @@ class AuthService {
11951229
11961230 /// Mode B convenience for Apple Sign-In. Returns
11971231 /// `(user: AuthUser, hasModeA: bool)` on success, `null` on user cancel.
1198- Future <({AuthUser user, bool hasModeA})?> signInAppleModeB ({required String password}) async {
1232+ Future <({AuthUser user, bool hasModeA})?> signInAppleModeB (
1233+ {required String password}) async {
11991234 try {
12001235 final credential = await SignInWithApple .getAppleIDCredential (
12011236 scopes: const [
@@ -1286,7 +1321,8 @@ class AuthService {
12861321
12871322 // Clear NFT wallet state and secure storage key
12881323 NftWalletService .instance.clear ();
1289- await SecureStorageService .instance.delete (SecureStorageKeys .nftWalletPrivateKey);
1324+ await SecureStorageService .instance
1325+ .delete (SecureStorageKeys .nftWalletPrivateKey);
12901326
12911327 // Clear NFT collections, received NFTs, and tags (user-specific data)
12921328 await NftService .instance.clearAll ();
@@ -1353,7 +1389,8 @@ class AuthService {
13531389 final account = await result.timeout (
13541390 const Duration (seconds: 5 ),
13551391 onTimeout: () {
1356- debugPrint ('Google lightweight auth timed out in reauthenticate - likely Android 16 Credential Manager issue' );
1392+ debugPrint (
1393+ 'Google lightweight auth timed out in reauthenticate - likely Android 16 Credential Manager issue' );
13571394 return null ;
13581395 },
13591396 );
0 commit comments