feat(auth): 接入 Google 登录
- 登录页保留邮箱与 Google 两个入口,Google 按钮按官方规范改为白底四色图标 - 新增 GoogleAuthService:封装 initialize/authenticate,失败以结果对象返回 - AuthApi 增加 googleLogin,AuthController 增加 signInWithGoogle - iOS 补充 CFBundleURLTypes 与 GIDClientID,Bundle ID 统一为 com.stormg.lumadrama - Google 请求期间按钮显示转圈并禁止重复点击
This commit is contained in:
1 parent
7b90bd59b9
commit
67670d46b1
18 files changed
+335
-55
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
@@ -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;
|
||||
|
||||
@@ -24,6 +24,19 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.11115008580-51gv4sj2c78rud6n127g3shu4d2n96un</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GIDClientID</key>
|
||||
<string>11115008580-51gv4sj2c78rud6n127g3shu4d2n96un.apps.googleusercontent.com</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -125,6 +125,29 @@ class AuthApi {
|
||||
return AuthSession.fromJson((data as Map).cast<String, dynamic>());
|
||||
}
|
||||
|
||||
/// Google 登录:把客户端拿到的 idToken 交给后端校验并换发令牌。
|
||||
///
|
||||
/// 账号按 Google 返回的邮箱归并:邮箱已存在则直接登入,不存在则自动建号。
|
||||
Future<AuthSession> 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<String, dynamic>());
|
||||
}
|
||||
|
||||
Future<AuthSession> refresh(
|
||||
String refreshToken, {
|
||||
String platform = '',
|
||||
|
||||
@@ -103,6 +103,17 @@ class AuthController extends ChangeNotifier {
|
||||
await _apply(session);
|
||||
}
|
||||
|
||||
/// Google 登录:把 Google 返回的 idToken 交给后端校验并落登录态。
|
||||
Future<void> signInWithGoogle(String idToken) async {
|
||||
final session = await _api.googleLogin(
|
||||
idToken: idToken,
|
||||
deviceId: await _store.deviceId(),
|
||||
platform: _platform,
|
||||
osVersion: _osVersion,
|
||||
);
|
||||
await _apply(session);
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
_session = null;
|
||||
await _store.clear();
|
||||
|
||||
@@ -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<GoogleSignInOutcome> 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<void> _ensureInitialized() async {
|
||||
if (_initialized) return;
|
||||
await _signIn.initialize(
|
||||
serverClientId: _isAndroid ? _serverClientId : null,
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
bool get _isAndroid => !kIsWeb && Platform.isAndroid;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -104,6 +104,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get authEmailSignIn => '邮箱登录';
|
||||
|
||||
@override
|
||||
String get authGoogleFailed => 'Google 登录失败,请稍后重试';
|
||||
|
||||
@override
|
||||
String get suspense => '悬疑';
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"authContinueGoogle": "使用 Google 继续",
|
||||
"authPhoneSignIn": "手机号登录",
|
||||
"authEmailSignIn": "邮箱登录",
|
||||
"authGoogleFailed": "Google 登录失败,请稍后重试",
|
||||
"suspense": "悬疑",
|
||||
"romance": "都市爱情",
|
||||
"actionGenre": "动作",
|
||||
|
||||
@@ -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<AuthPage> {
|
||||
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<AuthPage> {
|
||||
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<AuthPage> {
|
||||
showCenteredToast(context, failureMessage);
|
||||
}
|
||||
|
||||
/// Google 登录:先取 Google idToken,再交给后端换发本应用令牌。
|
||||
///
|
||||
/// 用户取消不提示错误;平台不支持或授权失败提示重试。
|
||||
Future<void> _signInWithGoogle() async {
|
||||
if (_logic.busy) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
unawaited(context.read<Haptics>().lightImpact());
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() {
|
||||
_logic.busy = true;
|
||||
_logic.errorMessage = null;
|
||||
});
|
||||
final auth = context.read<AuthController>();
|
||||
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<AuthPage> {
|
||||
),
|
||||
),
|
||||
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<AuthPage> {
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _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<Color>(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,
|
||||
|
||||
+57
-1
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Material>(
|
||||
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<Material>(
|
||||
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<Material>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('auth-entry-google')),
|
||||
matching: find.byType(Material),
|
||||
),
|
||||
);
|
||||
// 第三方登录按 Google 规范用白底深字。
|
||||
expect(googleButton.color, Colors.white);
|
||||
|
||||
final glow = tester.widget<DecoratedBox>(
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user