utils/crdt_sync_flutter/lib/src/google_any_sign_in.dart
Krzysztof kuhy Rudnicki 0f332c9057
Some checks are pending
crdt_sync_flutter tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
crdt_sync_flutter 0.4.0: AccountManager sign-in
Android now uses google_account_token (no googleid or
play-services-auth). Desktop PKCE sends client_secret.

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

103 lines
3.9 KiB
Dart

/// Picks the right Google sign-in flow for the platform.
///
/// Two flows exist because one is not enough: the `google_account_token`
/// plugin wraps Android's AccountManager, which has no desktop counterpart, so
/// a real GTK desktop build reaches Google through the OAuth loopback flow
/// instead. Both yield a Google ID token of the same shape, so
/// `signInWithGoogle` cannot tell them apart.
///
/// This file owns the choice so that consuming apps do not each re-derive it,
/// which is the whole reason this package exists.
library;
import 'package:crdt_sync_flutter/src/google_desktop_sign_in.dart';
import 'package:crdt_sync_flutter/src/google_platform.dart' as platform_gate;
import 'package:crdt_sync_flutter/src/google_sign_in.dart';
/// Whether Google sign-in can succeed here through *either* flow.
///
/// Pass this to `SyncSettingsScreen.googleAvailable`. Each half is gated on
/// its own client id being present, so an app that configured only one still
/// gets a button exactly where that one works.
///
/// [serverClientId] is the **Web** OAuth client id (the audience for the
/// plugin's tokens); [desktopClientId] is a **Desktop**-type client, the only
/// type permitted to redirect to loopback. Both are required, and passing ''
/// is how an app says it has not configured that half -- explicit, because a
/// silently-defaulted '' is how a button that can never succeed gets shipped.
bool googleAnySignInSupported({
required String serverClientId,
required String desktopClientId,
}) =>
googleSignInSupported(serverClientId) ||
desktopSignInSupported(desktopClientId);
/// Whether the loopback flow can run here.
///
/// True off the plugin's platforms, where a browser and a bindable socket are
/// what the flow actually needs. False on the plugin's own platforms, which
/// take the better native path, and on web, which can do neither.
bool desktopSignInSupported(String desktopClientId) =>
!platform_gate.googleSignInSupported &&
platform_gate.hasLoopbackBrowser &&
desktopClientId.isNotEmpty;
/// Which sign-in flow this platform should use.
///
/// A value rather than a branch inside the fetcher: the choice is the part
/// worth testing, and returning it lets the suite assert the routing on a
/// host that can run neither flow for real.
enum GoogleFlow {
/// The `google_account_token` plugin: Android.
plugin,
/// OAuth loopback in the system browser: Linux, Windows, macOS.
desktop,
/// Neither is available; the caller falls back to the password path.
none,
}
/// Picks the flow for this platform and configuration.
GoogleFlow googleFlowFor({
required String serverClientId,
required String desktopClientId,
}) {
if (googleSignInSupported(serverClientId)) return GoogleFlow.plugin;
if (desktopSignInSupported(desktopClientId)) return GoogleFlow.desktop;
return GoogleFlow.none;
}
/// Returns a Google ID token through whichever flow this platform supports.
///
/// Null when neither is available or the user backs out -- the caller falls
/// back to the password path either way.
///
/// [desktopClientSecret] is the Desktop client's secret (see
/// `googleDesktopIdToken`); '' sends none.
Future<String?> googleAnyIdToken({
required String serverClientId,
required String desktopClientId,
String desktopClientSecret = '',
}) async {
switch (googleFlowFor(
serverClientId: serverClientId,
desktopClientId: desktopClientId,
)) {
// Two one-line delegations, each reaching what `flutter test` cannot: a
// platform channel and a real browser. The decision above them -- which
// flow, and whether any runs at all -- is pure and fully covered.
// coverage:ignore-start
case GoogleFlow.plugin:
return await googleIdToken(serverClientId: serverClientId);
case GoogleFlow.desktop:
return await googleDesktopIdToken(
clientId: desktopClientId,
clientSecret: desktopClientSecret,
);
// coverage:ignore-end
case GoogleFlow.none:
return null;
}
}