mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
Use google_account_token for sign-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWY1k4muZfdVJfb7YGJ
This commit is contained in:
parent
5525ca6f95
commit
13f6e3f5d2
@ -1,11 +0,0 @@
|
||||
/// Platform gate for the programmatic Google sign-in flow.
|
||||
///
|
||||
/// Conditional export because the answer differs per platform *and* because
|
||||
/// asking the plugin directly is unsafe: `supportsAuthenticate()` throws
|
||||
/// `UnimplementedError` where no implementation is registered -- an `Error`,
|
||||
/// not an `Exception`, so it escapes an ordinary catch and would take down
|
||||
/// the settings screen's `build()`.
|
||||
library;
|
||||
|
||||
export 'google_platform_io.dart'
|
||||
if (dart.library.js_interop) 'google_platform_web.dart';
|
||||
@ -1,10 +0,0 @@
|
||||
/// Whether the programmatic Google sign-in flow exists on this platform.
|
||||
///
|
||||
/// The io half: Android has the Credential Manager flow; Linux desktop does
|
||||
/// not, and this app's desktop build is the web one anyway.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
/// True only on Android, the one platform shipping the programmatic flow.
|
||||
bool get googleSignInSupported => Platform.isAndroid;
|
||||
@ -1,8 +0,0 @@
|
||||
/// Whether the programmatic Google sign-in flow exists on this platform.
|
||||
///
|
||||
/// The web half: Google Identity Services signs in only through its own
|
||||
/// rendered button, so `authenticate()` throws `UnimplementedError` here.
|
||||
library;
|
||||
|
||||
/// Always false: the web plugin has no programmatic sign-in.
|
||||
bool get googleSignInSupported => false;
|
||||
@ -2,9 +2,9 @@
|
||||
///
|
||||
/// Why this is its own file: `crdt_sync` is deliberately pure Dart (only
|
||||
/// `crypto` and `http`), because the same library runs on Linux desktop and
|
||||
/// headless under systemd, where `google_sign_in` does not exist. So the
|
||||
/// plugin lives here, in the app, and the library only ever receives a token
|
||||
/// string through a closure.
|
||||
/// headless under systemd, where Android's AccountManager does not exist. So
|
||||
/// the plugin call lives here, in the app, and the library only ever receives
|
||||
/// a token string through a closure.
|
||||
///
|
||||
/// What this buys: on a fresh install the sync account is reached by tapping
|
||||
/// one account-picker row instead of typing a long password on a phone
|
||||
@ -12,10 +12,7 @@
|
||||
/// credential for desktop and the Python side.
|
||||
library;
|
||||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:wake_alarm_sync/services/google_platform.dart' as platform_gate;
|
||||
import 'package:google_account_token/google_account_token.dart' as plugin;
|
||||
|
||||
/// The account the security rules pin.
|
||||
///
|
||||
@ -50,73 +47,26 @@ const kServerClientId =
|
||||
|
||||
/// Whether this platform can sign in programmatically.
|
||||
///
|
||||
/// False on web, where Google Identity Services only signs in through its own
|
||||
/// rendered button: `authenticate()` throws `UnimplementedError` there. The
|
||||
/// desktop build of this app *is* the web build, so without this check the
|
||||
/// "Sign in with Google" button would crash the settings screen on desktop
|
||||
/// rather than falling back; see `google_platform.dart` for why this is a
|
||||
/// platform check rather than a question asked of the plugin.
|
||||
/// True only on Android (the plugin's own gate): the desktop build of this
|
||||
/// app *is* the web build, and neither has an AccountManager, so without this
|
||||
/// check the "Sign in with Google" button would show a flow that can never
|
||||
/// succeed instead of falling back to the password path.
|
||||
///
|
||||
/// Also false when no client id is compiled in: a button that cannot possibly
|
||||
/// succeed is worse than no button, and without this check an unconfigured
|
||||
/// build reports "cancelled" and sends you debugging the OAuth console for a
|
||||
/// problem that is really a missing constant.
|
||||
/// succeed is worse than no button.
|
||||
bool get googleSignInSupported =>
|
||||
platform_gate.googleSignInSupported && kServerClientId.isNotEmpty;
|
||||
plugin.googleAccountTokenSupported && kServerClientId.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.
|
||||
///
|
||||
/// [signInFn] and [serverClientId] exist for tests, which cannot reach the
|
||||
/// platform channel the plugin uses.
|
||||
/// [signInFn] and [serverClientId] exist for tests that bypass the plugin.
|
||||
Future<String?> googleIdToken({
|
||||
Future<String?> Function()? signInFn,
|
||||
String serverClientId = kServerClientId,
|
||||
}) async {
|
||||
if (signInFn != null) return await signInFn();
|
||||
if (serverClientId.isEmpty) {
|
||||
// A build without --dart-define=GOOGLE_SERVER_CLIENT_ID would otherwise
|
||||
// fail inside the plugin with a far less obvious message.
|
||||
log(
|
||||
'Google sign-in unavailable: GOOGLE_SERVER_CLIENT_ID was not set at '
|
||||
'build time; 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 -- the same reason main.dart and
|
||||
// firebase_backend.dart's keystore adapters are excluded. Everything above
|
||||
// this point is pure Dart and is covered.
|
||||
// coverage:ignore-start
|
||||
try {
|
||||
await GoogleSignIn.instance.initialize(serverClientId: serverClientId);
|
||||
if (!GoogleSignIn.instance.supportsAuthenticate()) {
|
||||
// Web/desktop: sign-in must go through the rendered GIS button, which
|
||||
// this app does not host yet. Fall back rather than throwing.
|
||||
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
|
||||
return await plugin.googleIdToken(serverClientId: serverClientId);
|
||||
}
|
||||
|
||||
@ -371,54 +371,15 @@ packages:
|
||||
url: "https://github.com/kuhyx/utils"
|
||||
source: git
|
||||
version: "0.3.2"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_sign_in:
|
||||
google_account_token:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in
|
||||
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
google_sign_in_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: "57782125965ca87b03d42a147d40b046f1fd6a09141a58913e1b86671a5ef22e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.16"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_platform_interface
|
||||
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
path: google_account_token
|
||||
ref: "google_account_token-v0.1.0"
|
||||
resolved-ref: db70ba204d124c6eadf9b8808cd3d3b3e70363d1
|
||||
url: "https://github.com/kuhyx/utils"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -33,7 +33,11 @@ dependencies:
|
||||
url: https://github.com/kuhyx/utils
|
||||
ref: github_device_auth-v0.3.2
|
||||
path: github_device_auth
|
||||
google_sign_in: 7.2.0
|
||||
google_account_token:
|
||||
git:
|
||||
url: https://github.com/kuhyx/utils
|
||||
ref: google_account_token-v0.1.0
|
||||
path: google_account_token
|
||||
http: 1.6.0
|
||||
path: 1.9.1
|
||||
path_provider: 2.1.6
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wake_alarm_sync/services/google_sign_in_backend.dart';
|
||||
|
||||
void main() {
|
||||
test('reports whether the programmatic Google flow exists here', () {
|
||||
// Android is the only platform shipping it. Asked as a platform question
|
||||
// rather than of the plugin, because GoogleSignIn.supportsAuthenticate()
|
||||
// itself throws UnimplementedError where no implementation is registered
|
||||
// -- an Error, not an Exception, so it would escape an ordinary catch and
|
||||
// take down the settings screen's build(). The test host is Linux, so
|
||||
// this is false here and the password path stays visible.
|
||||
expect(googleSignInSupported, isFalse);
|
||||
});
|
||||
}
|
||||
@ -1,15 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wake_alarm_sync/services/google_sign_in_backend.dart';
|
||||
|
||||
const _channel = MethodChannel('dev.kuhy/google_account_token');
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(_channel, null);
|
||||
});
|
||||
|
||||
group('googleIdToken', () {
|
||||
test('returns the token the sign-in function yields', () async {
|
||||
// The injected path: proves the closure is what feeds crdt_sync, and
|
||||
// that no platform channel is touched to get there.
|
||||
expect(
|
||||
await googleIdToken(signInFn: () async => 'a-token'),
|
||||
'a-token',
|
||||
);
|
||||
expect(await googleIdToken(signInFn: () async => 'a-token'), 'a-token');
|
||||
});
|
||||
|
||||
test('passes a null cancellation straight through', () async {
|
||||
@ -19,12 +28,24 @@ void main() {
|
||||
});
|
||||
|
||||
test('reports "not configured" when the client id is missing', () async {
|
||||
// Passed explicitly rather than relying on the const: an app whose
|
||||
// client id is filled in would otherwise reach the real plugin here.
|
||||
// The behaviour under test is the empty-id fallback itself.
|
||||
// An empty client id must fall back rather than reach the OS chooser.
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
expect(await googleIdToken(serverClientId: ''), isNull);
|
||||
});
|
||||
|
||||
test('asks the account-token plugin for the Web client id', () async {
|
||||
// The real path: the plugin's channel gets the pinned Web client id.
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
Object? sent;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(_channel, (call) async {
|
||||
sent = call.arguments;
|
||||
return 'plugin-token';
|
||||
});
|
||||
expect(await googleIdToken(), 'plugin-token');
|
||||
expect(sent, {'serverClientId': kServerClientId});
|
||||
});
|
||||
|
||||
test('pins the uid the security rules expect', () {
|
||||
// If this ever drifts from database.rules.json, a Google sign-in would
|
||||
// be accepted here and then denied every read and write.
|
||||
|
||||
19
phone_app/test/google_sign_in_supported_test.dart
Normal file
19
phone_app/test/google_sign_in_supported_test.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wake_alarm_sync/services/google_sign_in_backend.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
|
||||
test('the one-tap button is hidden off Android', () {
|
||||
// flutter_test defaults to Android, so pin Linux (the desktop build): no
|
||||
// AccountManager, so the password path stays the only option.
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
expect(googleSignInSupported, isFalse);
|
||||
});
|
||||
|
||||
test('the one-tap button shows on Android', () {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
expect(googleSignInSupported, isTrue);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user