mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-14 11:10:58 +01:00
Five apps had copied google_sign_in_backend.dart; home_inventory's copy was byte-identical to todo's. lyricanki was about to become the sixth, which is the mistake this package exists to prevent. So the plugin call moves here. Desktop is the new half. google_sign_in ships android, ios and web implementations only -- there is no google_sign_in_linux -- so lyricanki's real GTK build could never have used the plugin, and "one tap" would have meant "one tap on the phone, type a password on the PC". The OAuth loopback flow covers it: bind a socket, open the system browser, catch the redirect, exchange the code with PKCE. Both halves return a Google ID token of the same shape, so signInWithGoogle cannot tell them apart. googleSignInSupported takes the client id rather than reading a const. In todo the emptiness check was a second conjunct at the call site, and moving the fetcher here without it would have let a forgetful app ship a button that always reports "cancelled" -- todo's own file documents that failure. Taking the value as an argument makes the check impossible to drop. The decision (which flow, or none) is pure and covered; the two one-line delegations that reach a platform channel and a real browser are the only suppressions. 83 tests, 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
89 lines
3.6 KiB
Dart
89 lines
3.6 KiB
Dart
/// One-tap Google sign-in: the `google_sign_in` plugin call, owned once.
|
|
///
|
|
/// Why it lives here and not in `crdt_sync`: that package is pure Dart on
|
|
/// purpose, because the same library runs on Linux desktop and headless under
|
|
/// systemd, where `google_sign_in` does not exist. It only ever receives a
|
|
/// token string through a closure -- see `signInWithGoogle`, which takes a
|
|
/// `tokenFetcher`. This file is that closure.
|
|
///
|
|
/// Why it lives here and not in each app: five apps had copied it, and
|
|
/// `home_inventory`'s copy was byte-identical to `todo`'s. Copying it a sixth
|
|
/// time is the mistake this package exists to prevent.
|
|
library;
|
|
|
|
import 'dart:developer';
|
|
|
|
import 'package:crdt_sync_flutter/src/google_platform.dart' as platform_gate;
|
|
import 'package:google_sign_in/google_sign_in.dart';
|
|
|
|
/// Whether this build can sign in with Google programmatically.
|
|
///
|
|
/// Two conjuncts, both load-bearing:
|
|
///
|
|
/// * the platform must ship the flow (see `google_platform.dart`);
|
|
/// * [serverClientId] must be non-empty. A build that forgot the constant
|
|
/// would otherwise show a button that always reports "cancelled" and send
|
|
/// you debugging the OAuth console for a missing string.
|
|
///
|
|
/// Takes the id rather than reading a package-level const so the second check
|
|
/// cannot be dropped by a consuming app: the only way to ask the question is
|
|
/// to supply the value the answer depends on.
|
|
bool googleSignInSupported(String serverClientId) =>
|
|
platform_gate.googleSignInSupported && serverClientId.isNotEmpty;
|
|
|
|
/// Returns a Google ID token for the signed-in account, or null.
|
|
///
|
|
/// Null rather than throwing when the user dismisses the picker: cancelling is
|
|
/// an ordinary outcome, and the caller falls back to the password path.
|
|
///
|
|
/// [serverClientId] must be the project's **Web** OAuth client id -- Android
|
|
/// must request a token minted for the web client, and an Android client id
|
|
/// yields a token Firebase rejects with `audience mismatch`.
|
|
///
|
|
/// [signInFn] exists for tests, which cannot reach the plugin's platform
|
|
/// channel; when supplied it replaces the plugin entirely.
|
|
Future<String?> googleIdToken({
|
|
required String serverClientId,
|
|
Future<String?> Function()? signInFn,
|
|
}) async {
|
|
if (signInFn != null) return signInFn();
|
|
if (serverClientId.isEmpty) {
|
|
log(
|
|
'Google sign-in unavailable: no server client id was compiled in; '
|
|
'falling back to the password path',
|
|
level: 900,
|
|
);
|
|
return null;
|
|
}
|
|
// The plugin reaches the OS account picker through a platform channel,
|
|
// which `flutter test` has no binding for. Everything above this point is
|
|
// pure Dart and is covered.
|
|
// coverage:ignore-start
|
|
try {
|
|
await GoogleSignIn.instance.initialize(serverClientId: serverClientId);
|
|
if (!GoogleSignIn.instance.supportsAuthenticate()) {
|
|
log(
|
|
'Google sign-in is not available on this platform; falling back to '
|
|
'the password path',
|
|
level: 900,
|
|
);
|
|
return null;
|
|
}
|
|
final account = await GoogleSignIn.instance.authenticate();
|
|
return account.authentication.idToken;
|
|
} on GoogleSignInException catch (error, stackTrace) {
|
|
// Includes the user simply dismissing the picker, which is not an error
|
|
// worth surfacing -- but log it, because a *configuration* failure
|
|
// (unregistered SHA-1, wrong client id) arrives through the same path and
|
|
// is otherwise indistinguishable from "the user changed their mind".
|
|
log(
|
|
'Google sign-in did not complete',
|
|
level: 900,
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
return null;
|
|
}
|
|
// coverage:ignore-end
|
|
}
|