mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
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
85 lines
3.1 KiB
Dart
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;
|
|
}
|
|
}
|