diff --git a/assets/icon/google_g.png b/assets/icon/google_g.png
new file mode 100644
index 0000000..8d17e48
Binary files /dev/null and b/assets/icon/google_g.png differ
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 7609503..233b1fe 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -387,7 +387,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadramv;
+ PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadrama;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -567,7 +567,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadramv;
+ PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadrama;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -590,7 +590,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadramv;
+ PRODUCT_BUNDLE_IDENTIFIER = com.stormg.lumadrama;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 25c3834..4c7589a 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -24,6 +24,19 @@
????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ com.googleusercontent.apps.11115008580-51gv4sj2c78rud6n127g3shu4d2n96un
+
+
+
+ GIDClientID
+ 11115008580-51gv4sj2c78rud6n127g3shu4d2n96un.apps.googleusercontent.com
LSRequiresIPhoneOS
NSAppTransportSecurity
diff --git a/lib/core/data/remote/auth_api.dart b/lib/core/data/remote/auth_api.dart
index 603e181..e15e53e 100644
--- a/lib/core/data/remote/auth_api.dart
+++ b/lib/core/data/remote/auth_api.dart
@@ -125,6 +125,29 @@ class AuthApi {
return AuthSession.fromJson((data as Map).cast());
}
+ /// Google 登录:把客户端拿到的 idToken 交给后端校验并换发令牌。
+ ///
+ /// 账号按 Google 返回的邮箱归并:邮箱已存在则直接登入,不存在则自动建号。
+ Future googleLogin({
+ required String idToken,
+ required String deviceId,
+ String platform = '',
+ String osVersion = '',
+ String appVersion = '',
+ }) async {
+ final data = await _client.post(
+ '/app/auth/google',
+ body: {
+ 'idToken': idToken,
+ 'deviceId': deviceId,
+ 'platform': platform,
+ 'osVersion': osVersion,
+ 'appVersion': appVersion,
+ },
+ );
+ return AuthSession.fromJson((data as Map).cast());
+ }
+
Future refresh(
String refreshToken, {
String platform = '',
diff --git a/lib/core/data/services/auth_controller.dart b/lib/core/data/services/auth_controller.dart
index 12ed578..442b497 100644
--- a/lib/core/data/services/auth_controller.dart
+++ b/lib/core/data/services/auth_controller.dart
@@ -103,6 +103,17 @@ class AuthController extends ChangeNotifier {
await _apply(session);
}
+ /// Google 登录:把 Google 返回的 idToken 交给后端校验并落登录态。
+ Future signInWithGoogle(String idToken) async {
+ final session = await _api.googleLogin(
+ idToken: idToken,
+ deviceId: await _store.deviceId(),
+ platform: _platform,
+ osVersion: _osVersion,
+ );
+ await _apply(session);
+ }
+
Future clear() async {
_session = null;
await _store.clear();
diff --git a/lib/core/data/services/google_auth_service.dart b/lib/core/data/services/google_auth_service.dart
new file mode 100644
index 0000000..aaaa5e6
--- /dev/null
+++ b/lib/core/data/services/google_auth_service.dart
@@ -0,0 +1,97 @@
+// Author: fruit.com.cn
+
+import 'dart:io' show Platform;
+
+import 'package:flutter/foundation.dart';
+import 'package:google_sign_in/google_sign_in.dart';
+
+/// Google 授权结局。
+///
+/// 插件调用与 IO 一样用结果对象返回,不把异常抛到页面:
+/// 页面只需要区分"成功 / 用户取消 / 不可用 / 失败"四种情况。
+class GoogleSignInOutcome {
+ const GoogleSignInOutcome._({
+ this.idToken,
+ this.canceled = false,
+ this.unavailable = false,
+ this.failed = false,
+ });
+
+ /// 成功拿到可交给后端校验的 idToken。
+ const GoogleSignInOutcome.success(String idToken) : this._(idToken: idToken);
+
+ /// 用户主动取消授权,页面不应提示错误。
+ const GoogleSignInOutcome.canceled() : this._(canceled: true);
+
+ /// 当前平台或构建不支持交互式登录。
+ const GoogleSignInOutcome.unavailable() : this._(unavailable: true);
+
+ /// 授权过程失败(网络、配置缺失、Google 拒绝等)。
+ const GoogleSignInOutcome.failed() : this._(failed: true);
+
+ /// Google 返回的 idToken;未成功时为 null。
+ final String? idToken;
+
+ /// 用户是否主动取消。
+ final bool canceled;
+
+ /// 平台是否不支持。
+ final bool unavailable;
+
+ /// 是否失败。
+ final bool failed;
+
+ /// 是否拿到了可用的 idToken。
+ bool get isSuccess => idToken != null && idToken!.isNotEmpty;
+}
+
+/// Google 登录客户端封装。
+///
+/// 只负责拉起 Google 授权并取出 idToken;换发本应用令牌由 AuthController 完成,
+/// 插件调用与网络层各自独立,页面通过 [GoogleSignInOutcome] 判断结局。
+class GoogleAuthService {
+ GoogleAuthService({GoogleSignIn? signIn})
+ : _signIn = signIn ?? GoogleSignIn.instance;
+
+ /// Android 必须显式传 serverClientId 才会返回 idToken,且要传 Web 客户端 id;
+ /// iOS 由 Info.plist 的 GIDClientID 决定,无需再传。
+ static const String _serverClientId =
+ '11115008580-dbqe47fhp5560nhrocrppiq7hcnlvl1s.apps.googleusercontent.com';
+
+ final GoogleSignIn _signIn;
+ bool _initialized = false;
+
+ /// 拉起授权并返回结局;不会抛异常。
+ Future signIn() async {
+ try {
+ await _ensureInitialized();
+ if (!_signIn.supportsAuthenticate()) {
+ return const GoogleSignInOutcome.unavailable();
+ }
+ final account = await _signIn.authenticate();
+ final idToken = account.authentication.idToken;
+ if (idToken == null || idToken.isEmpty) {
+ return const GoogleSignInOutcome.failed();
+ }
+ return GoogleSignInOutcome.success(idToken);
+ } on GoogleSignInException catch (error) {
+ if (error.code == GoogleSignInExceptionCode.canceled) {
+ return const GoogleSignInOutcome.canceled();
+ }
+ return const GoogleSignInOutcome.failed();
+ } catch (_) {
+ return const GoogleSignInOutcome.failed();
+ }
+ }
+
+ /// 插件要求先 initialize 才能 authenticate;只做一次。
+ Future _ensureInitialized() async {
+ if (_initialized) return;
+ await _signIn.initialize(
+ serverClientId: _isAndroid ? _serverClientId : null,
+ );
+ _initialized = true;
+ }
+
+ bool get _isAndroid => !kIsWeb && Platform.isAndroid;
+}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 4633e72..485612e 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -32,6 +32,7 @@
"authContinueGoogle": "Continue with Google",
"authPhoneSignIn": "Phone sign in",
"authEmailSignIn": "Email sign in",
+ "authGoogleFailed": "Google sign in failed, please try again",
"suspense": "Suspense",
"romance": "Romance",
"actionGenre": "Action",
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 1f9f6d6..7bda80a 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -32,6 +32,7 @@
"authContinueGoogle": "Lanjutkan dengan Google",
"authPhoneSignIn": "Masuk dengan ponsel",
"authEmailSignIn": "Masuk dengan email",
+ "authGoogleFailed": "Gagal masuk dengan Google, silakan coba lagi",
"suspense": "Thriller",
"romance": "Romantis",
"actionGenre": "Aksi",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index ed98fdc..99c011d 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -292,6 +292,12 @@ abstract class AppLocalizations {
/// **'Email sign in'**
String get authEmailSignIn;
+ /// No description provided for @authGoogleFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Google sign in failed, please try again'**
+ String get authGoogleFailed;
+
/// No description provided for @suspense.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 58cdf14..023579c 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -106,6 +106,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get authEmailSignIn => 'Email sign in';
+ @override
+ String get authGoogleFailed => 'Google sign in failed, please try again';
+
@override
String get suspense => 'Suspense';
diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart
index d77c7ff..51724b3 100644
--- a/lib/l10n/app_localizations_id.dart
+++ b/lib/l10n/app_localizations_id.dart
@@ -106,6 +106,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get authEmailSignIn => 'Masuk dengan email';
+ @override
+ String get authGoogleFailed => 'Gagal masuk dengan Google, silakan coba lagi';
+
@override
String get suspense => 'Thriller';
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index 12b9a6c..f202190 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -104,6 +104,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get authEmailSignIn => '邮箱登录';
+ @override
+ String get authGoogleFailed => 'Google 登录失败,请稍后重试';
+
@override
String get suspense => '悬疑';
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 09377b6..84eec2c 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -32,6 +32,7 @@
"authContinueGoogle": "使用 Google 继续",
"authPhoneSignIn": "手机号登录",
"authEmailSignIn": "邮箱登录",
+ "authGoogleFailed": "Google 登录失败,请稍后重试",
"suspense": "悬疑",
"romance": "都市爱情",
"actionGenre": "动作",
diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart
index 4970a42..dc0b360 100644
--- a/lib/pages/auth/auth_view.dart
+++ b/lib/pages/auth/auth_view.dart
@@ -9,6 +9,7 @@ import '../../core/data/remote/api_client.dart';
import '../../core/formatting/error_text.dart';
import '../../core/data/services/app_config_controller.dart';
import '../../core/data/services/auth_controller.dart';
+import '../../core/data/services/google_auth_service.dart';
import '../../core/platform/haptics.dart';
import '../../core/platform/keyboard_inset.dart';
import '../../core/theme/app_colors.dart';
@@ -20,7 +21,6 @@ import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import 'auth_legal_links.dart';
import 'auth_logic.dart';
-import 'phone_auth_sheet.dart';
part 'auth_widgets.dart';
part 'auth_form_widgets.dart';
@@ -31,6 +31,7 @@ const double _compactBrandMarkSize = 48;
const double _brandMarkRadius = 26;
const double _compactBrandMarkRadius = 14;
const String _appLogoAsset = 'assets/icon/ymb.png';
+const String _googleIconAsset = 'assets/icon/google_g.png';
const double _contentMaxWidth = 440;
const double _fieldHeight = 52;
const double _fieldRadius = 12;
@@ -49,6 +50,8 @@ const double _entryButtonHeight = 52;
const double _entryButtonRadius = 26;
const double _entryButtonFontSize = 15;
const double _entryIconSize = 20;
+const double _loadingStrokeWidth = 2;
+const Color _googleOnLight = Color(0xFF1F1F1F);
const Duration _enterDuration = Duration(milliseconds: 220);
Widget _brandMarkWidget({
@@ -80,6 +83,7 @@ class AuthPage extends StatefulWidget {
class _AuthPageState extends State {
late AuthLogic _logic;
+ late final GoogleAuthService _googleAuth;
late final TextEditingController _emailController;
late final TextEditingController _passwordController;
late final FocusNode _emailFocus;
@@ -94,6 +98,7 @@ class _AuthPageState extends State {
void initState() {
super.initState();
_logic = AuthLogic(signUpMode: widget.signUpMode);
+ _googleAuth = GoogleAuthService();
_emailController = TextEditingController();
_passwordController = TextEditingController();
_emailFocus = FocusNode(debugLabel: 'auth-email-focus');
@@ -248,6 +253,42 @@ class _AuthPageState extends State {
showCenteredToast(context, failureMessage);
}
+ /// Google 登录:先取 Google idToken,再交给后端换发本应用令牌。
+ ///
+ /// 用户取消不提示错误;平台不支持或授权失败提示重试。
+ Future _signInWithGoogle() async {
+ if (_logic.busy) return;
+ final l10n = AppLocalizations.of(context)!;
+ unawaited(context.read().lightImpact());
+ FocusScope.of(context).unfocus();
+ setState(() {
+ _logic.busy = true;
+ _logic.errorMessage = null;
+ });
+ final auth = context.read();
+ String? failureMessage;
+ try {
+ final outcome = await _googleAuth.signIn();
+ if (!mounted) return;
+ if (outcome.canceled) return;
+ if (!outcome.isSuccess) {
+ failureMessage = l10n.authGoogleFailed;
+ } else {
+ await auth.signInWithGoogle(outcome.idToken!);
+ if (!mounted) return;
+ Navigator.of(context).maybePop();
+ }
+ } on ApiException catch (error) {
+ failureMessage = userFacingApiMessage(l10n, error.code, error.message);
+ } catch (_) {
+ failureMessage = l10n.authNetworkError;
+ } finally {
+ if (mounted) setState(() => _logic.busy = false);
+ }
+ if (!mounted || failureMessage == null) return;
+ showCenteredToast(context, failureMessage);
+ }
+
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
@@ -437,21 +478,23 @@ class _AuthPageState extends State {
),
),
const Spacer(flex: 2),
- _AuthEntryButton(
- key: const Key('auth-entry-phone'),
- label: l10n.authPhoneSignIn,
- icon: Icons.phone_android_rounded,
- primary: true,
- onTap: _openPhoneSheet,
- ),
- const SizedBox(height: 12),
_AuthEntryButton(
key: const Key('auth-entry-email'),
label: l10n.authEmailSignIn,
icon: Icons.mail_outline_rounded,
- primary: false,
+ primary: true,
onTap: () => setState(() => _emailEntryOpen = true),
),
+ const SizedBox(height: 12),
+ _AuthEntryButton(
+ key: const Key('auth-entry-google'),
+ label: l10n.authContinueGoogle,
+ imageAsset: _googleIconAsset,
+ light: true,
+ primary: false,
+ loading: _logic.busy,
+ onTap: _signInWithGoogle,
+ ),
const SizedBox(height: 14),
const Opacity(opacity: 0.75, child: AuthLegalLinks()),
const Spacer(flex: 6),
@@ -459,14 +502,6 @@ class _AuthPageState extends State {
),
);
- Future _openPhoneSheet() async {
- final signedIn = await showPhoneAuthSheet(
- context,
- signUp: _logic.signUpMode,
- );
- if (signedIn == true && mounted) Navigator.of(context).maybePop();
- }
-
Widget _authCard(AppLocalizations l10n) => _AuthCard(
state: _AuthCardState(
l10n: l10n,
@@ -513,6 +548,9 @@ class _AuthEntryButton extends StatelessWidget {
required this.primary,
required this.onTap,
this.icon,
+ this.imageAsset,
+ this.light = false,
+ this.loading = false,
});
final String label;
@@ -520,13 +558,24 @@ class _AuthEntryButton extends StatelessWidget {
final VoidCallback onTap;
final IconData? icon;
+ /// 需要显示彩色品牌图标时用图片;与 [icon] 二选一。
+ final String? imageAsset;
+
+ /// 白底深字样式(Google 官方按钮规范),用于第三方登录入口。
+ final bool light;
+
+ /// 请求进行中:显示转圈并禁止重复点击。
+ final bool loading;
+
@override
Widget build(BuildContext context) {
- // 主按钮纯品牌红;次按钮为无边框深色玻璃(白 6%),Apple 风格。
+ // 主按钮纯品牌红;次按钮为无边框深色玻璃(白 6%);第三方登录用白底深字。
final background = primary
? AppColors.brandRed
- : Colors.white.withValues(alpha: 0.06);
- final foreground = primary ? AppColors.onPrimary : AppColors.onSurface;
+ : (light ? Colors.white : Colors.white.withValues(alpha: 0.06));
+ final foreground = primary
+ ? AppColors.onPrimary
+ : (light ? _googleOnLight : AppColors.onSurface);
return Material(
color: background,
shape: RoundedRectangleBorder(
@@ -534,16 +583,34 @@ class _AuthEntryButton extends StatelessWidget {
),
clipBehavior: Clip.antiAlias,
child: InkWell(
- onTap: onTap,
+ onTap: loading ? null : onTap,
borderRadius: BorderRadius.circular(_entryButtonRadius),
child: SizedBox(
height: _entryButtonHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- if (icon != null)
+ if (loading) ...[
+ SizedBox(
+ width: _entryIconSize,
+ height: _entryIconSize,
+ child: CircularProgressIndicator(
+ strokeWidth: _loadingStrokeWidth,
+ valueColor: AlwaysStoppedAnimation(foreground),
+ ),
+ ),
+ const SizedBox(width: 10),
+ ] else if (imageAsset != null) ...[
+ Image.asset(
+ imageAsset!,
+ width: _entryIconSize,
+ height: _entryIconSize,
+ ),
+ const SizedBox(width: 10),
+ ] else if (icon != null) ...[
Icon(icon, size: _entryIconSize, color: foreground),
- if (icon != null) const SizedBox(width: 8),
+ const SizedBox(width: 8),
+ ],
Flexible(
child: Text(
label,
diff --git a/pubspec.lock b/pubspec.lock
index eec0a23..d29a50f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -207,6 +207,54 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ google_identity_services_web:
+ dependency: transitive
+ description:
+ name: google_identity_services_web
+ sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.3.3+1"
+ google_sign_in:
+ dependency: "direct main"
+ description:
+ name: google_sign_in
+ sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "7.2.0"
+ google_sign_in_android:
+ dependency: transitive
+ description:
+ name: google_sign_in_android
+ sha256: af51a005b7c3c3f54cb5e7d04dd5db96f081e6f92b36468c60d14726a047f918
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "7.2.19"
+ google_sign_in_ios:
+ dependency: transitive
+ description:
+ name: google_sign_in_ios
+ sha256: bc03e8f8cf0e5a5cbbc1d582516e5f792c734f0a41d324d4760dcb8bef523b28
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "6.4.0"
+ google_sign_in_platform_interface:
+ dependency: transitive
+ description:
+ name: google_sign_in_platform_interface
+ sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.1.0"
+ google_sign_in_web:
+ dependency: transitive
+ description:
+ name: google_sign_in_web
+ sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.1.3"
hooks:
dependency: transitive
description:
@@ -223,6 +271,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.15.7"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.6.0"
http_parser:
dependency: transitive
description:
@@ -779,4 +835,4 @@ packages:
version: "3.1.4"
sdks:
dart: ">=3.13.0 <4.0.0"
- flutter: ">=3.44.0"
+ flutter: ">=3.47.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 10dcd44..2f1f3bb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -51,6 +51,7 @@ dependencies:
floating: ^6.0.0
# 运行时权限:systemAlertWindow(「显示在其他应用上层」)
permission_handler: ^13.0.2
+ google_sign_in: ^7.2.0
dev_dependencies:
flutter_test:
diff --git a/test/auth/auth_landing_test.dart b/test/auth/auth_landing_test.dart
index 75e6ff6..b97e57e 100644
--- a/test/auth/auth_landing_test.dart
+++ b/test/auth/auth_landing_test.dart
@@ -53,22 +53,15 @@ void main() {
);
await tester.pumpAndSettle();
- expect(find.byKey(const Key('auth-entry-google')), findsNothing);
- expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget);
+ expect(find.byKey(const Key('auth-entry-phone')), findsNothing);
expect(find.byKey(const Key('auth-entry-email')), findsOneWidget);
+ expect(find.byKey(const Key('auth-entry-google')), findsOneWidget);
expect(find.byKey(const Key('auth-legal')), findsOneWidget);
expect(
tester.getTopLeft(find.byKey(const Key('auth-landing'))).dy,
lessThan(80),
);
- await tester.tap(find.byKey(const Key('auth-entry-phone')));
- await tester.pumpAndSettle();
- expect(find.byKey(const Key('phone-auth-sheet')), findsOneWidget);
-
- await tester.tapAt(const Offset(8, 8));
- await tester.pumpAndSettle();
-
await tester.tap(find.byKey(const Key('auth-entry-email')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('auth-email')), findsOneWidget);
diff --git a/test/auth/auth_visual_test.dart b/test/auth/auth_visual_test.dart
index def9a26..df909bc 100644
--- a/test/auth/auth_visual_test.dart
+++ b/test/auth/auth_visual_test.dart
@@ -90,28 +90,17 @@ void main() {
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
const Size(88, 88),
);
- expect(find.byKey(const Key('auth-entry-google')), findsNothing);
- expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget);
+ expect(find.byKey(const Key('auth-entry-google')), findsOneWidget);
+ expect(find.byKey(const Key('auth-entry-phone')), findsNothing);
expect(find.byKey(const Key('auth-entry-email')), findsOneWidget);
expect(find.byKey(const Key('auth-legal')), findsOneWidget);
- expect(
- tester.getSize(find.byKey(const Key('auth-entry-phone'))).height,
- 52,
- );
expect(
tester.getSize(find.byKey(const Key('auth-entry-email'))).height,
52,
);
- final phoneButton = tester.widget(
- find.descendant(
- of: find.byKey(const Key('auth-entry-phone')),
- matching: find.byType(Material),
- ),
- );
- expect(phoneButton.color, AppColors.brandRed);
expect(
- (phoneButton.shape! as RoundedRectangleBorder).borderRadius,
- BorderRadius.circular(26),
+ tester.getSize(find.byKey(const Key('auth-entry-google'))).height,
+ 52,
);
final emailButton = tester.widget(
find.descendant(
@@ -119,8 +108,20 @@ void main() {
matching: find.byType(Material),
),
);
- // 次按钮是「白 6%」玻璃拟态,不再有描边。
- expect(emailButton.color, Colors.white.withValues(alpha: 0.06));
+ // 邮箱是主入口,用品牌红。
+ expect(emailButton.color, AppColors.brandRed);
+ expect(
+ (emailButton.shape! as RoundedRectangleBorder).borderRadius,
+ BorderRadius.circular(26),
+ );
+ final googleButton = tester.widget(
+ find.descendant(
+ of: find.byKey(const Key('auth-entry-google')),
+ matching: find.byType(Material),
+ ),
+ );
+ // 第三方登录按 Google 规范用白底深字。
+ expect(googleButton.color, Colors.white);
final glow = tester.widget(
find.byKey(const Key('auth-red-glow')),
@@ -237,7 +238,7 @@ void main() {
expect(tester.getCenter(logo).dx, closeTo(195, 0.1));
expect(
tester.getBottomLeft(logo).dy,
- lessThan(tester.getTopLeft(find.byKey(const Key('auth-entry-phone'))).dy),
+ lessThan(tester.getTopLeft(find.byKey(const Key('auth-entry-email'))).dy),
);
});