utils/google_account_token/lib/google_account_token.dart
Krzysztof kuhy Rudnicki db70ba204d Add google_account_token plugin
Android AccountManager ID token for Firebase signInWithIdp, extracted from todo so the sync apps stop copying the Kotlin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWY1k4muZfdVJfb7YGJ
2026-11-02 10:32:58 +01:00

85 lines
3.1 KiB
Dart

/// One-tap Google ID token on Android, owned once for every app.
///
/// The system account chooser (`AccountManager.newChooseAccountIntent`), then
/// `getAuthToken(account, "audience:server:client_id:<web client id>")`. The
/// result is a Google ID token Firebase `signInWithIdp` accepts: one dialog,
/// no browser, no Google client library, no Play Services SDK in the APK.
///
/// Why a plugin: the Kotlin used to be copied into each app, and per-repo
/// OAuth code drifts. Apps depend on this package instead and keep only their
/// own constants (the pinned uid and the Web client id).
///
/// Other platforms (web, Linux desktop, iOS) have no AccountManager.
/// [googleIdToken] returns null there, so the caller falls back to its
/// password path exactly as it does after the user dismisses the chooser.
library;
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// The channel `GoogleAccountTokenPlugin.kt` listens on.
const kGoogleAccountTokenChannel = MethodChannel(
'dev.kuhy/google_account_token',
);
/// Whether this platform can produce a token at all.
///
/// True only on Android. Use it to hide a "Sign in with Google" button that
/// could never succeed, rather than showing one that always reports
/// "cancelled".
bool get googleAccountTokenSupported =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
/// A Google ID token for the signed-in account, or null.
///
/// Null rather than throwing for every ordinary outcome: the user dismissed
/// the chooser, the platform is unsupported, [serverClientId] is empty, the
/// native side is not registered, or the token request failed. Failures are
/// logged (level 900) because a configuration error (unregistered SHA-1,
/// wrong client id) arrives through the same path as "the user changed their
/// mind" and is otherwise indistinguishable.
///
/// [serverClientId] must be the project's **Web** OAuth client id. An Android
/// client id yields a token Firebase rejects with `audience mismatch`.
///
/// [accountHint] is an account name (an email) pre-selected in the chooser.
/// The chooser is always shown, so this saves a tap but never skips consent.
Future<String?> googleIdToken({
required String serverClientId,
String? accountHint,
}) async {
if (!googleAccountTokenSupported) return null;
if (serverClientId.isEmpty) {
log(
'Google sign-in unavailable: no server client id; '
'falling back to the password path',
level: 900,
);
return null;
}
try {
return await kGoogleAccountTokenChannel.invokeMethod<String>('idToken', {
'serverClientId': serverClientId,
'accountHint': ?accountHint,
});
} on PlatformException catch (error, stackTrace) {
log(
'AccountManager token failed',
level: 900,
error: error,
stackTrace: stackTrace,
);
return null;
} on MissingPluginException catch (error, stackTrace) {
log(
'google_account_token native side is not registered',
level: 900,
error: error,
stackTrace: stackTrace,
);
return null;
}
}