gcx/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart

419 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
import 'package:fluwx/fluwx.dart';
import 'package:tobias/tobias.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/genex_button.dart';
import '../../../../app/i18n/app_localizations.dart';
import '../../../../core/services/auth_service.dart';
/// A1. 欢迎页 - 品牌展示 + 注册/登录入口
///
/// 平台差异(运行时判断):
/// Android: 微信 + 支付宝 + Google
/// iOS: 微信 + 支付宝 + Google + Apple
class WelcomePage extends StatefulWidget {
const WelcomePage({super.key});
@override
State<WelcomePage> createState() => _WelcomePageState();
}
class _WelcomePageState extends State<WelcomePage> {
bool _wechatLoading = false;
bool _alipayLoading = false;
bool _googleLoading = false;
bool _appleLoading = false;
final _googleSignIn = GoogleSignIn(scopes: ['email', 'profile']);
@override
void initState() {
super.initState();
// 监听微信授权回调(用户授权后,微信 App 返回 code
weChatResponseEventHandler.distinct().listen((res) {
if (res is WXAuthResp && mounted) {
_handleWechatAuthResp(res);
}
});
}
// ── 微信 ─────────────────────────────────────────────────────────────────
Future<void> _onWechatTap() async {
final installed = await isWeChatInstalled;
if (!installed) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.wechatNotInstalled'))),
);
}
return;
}
setState(() => _wechatLoading = true);
await sendWeChatAuth(scope: 'snsapi_userinfo', state: 'genex_login');
// 授权结果通过 weChatResponseEventHandler 异步回调
}
Future<void> _handleWechatAuthResp(WXAuthResp resp) async {
setState(() => _wechatLoading = false);
if (resp.errCode != 0 || resp.code == null) return;
try {
await AuthService.instance.loginByWechat(code: resp.code!);
if (mounted) Navigator.pushReplacementNamed(context, '/main');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.wechatLoginFailed'))),
);
}
}
}
// ── 支付宝 ─────────────────────────────────────────────────────────────────
// tobias 包使用方法:
// isAliPayInstalled() → Future<bool>
// aliPay(String orderStr) — 用于支付
// 但 OAuth 授权用的是 Alipay.authCode(appId, scope) 返回 auth_code
//
// tobias v3.x API: aliPayAuth(appId, scope) → Future<Map>
// 返回 {'resultStatus': '9000', 'memo': '', 'result': '...(含 auth_code)...'}
// resultStatus: 9000=成功, 6001=取消, 4000=错误
Future<void> _onAlipayTap() async {
final installed = await isAliPayInstalled;
if (!installed) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.alipayNotInstalled'))),
);
}
return;
}
setState(() => _alipayLoading = true);
try {
// appId 与后端 ALIPAY_APP_ID 一致
const alipayAppId = String.fromEnvironment('ALIPAY_APP_ID', defaultValue: '');
final result = await aliPayAuth(alipayAppId, 'auth_user');
final status = result['resultStatus']?.toString() ?? '';
if (status != '9000') {
// 用户取消或错误
return;
}
// 从 result 字符串中解析 auth_code
// result 格式: "auth_code=AP_xxxxxxxx&scope=auth_user&state=..."
final resultStr = result['result']?.toString() ?? '';
final authCode = _parseParam(resultStr, 'auth_code');
if (authCode == null || authCode.isEmpty) {
throw Exception('未获取到 auth_code');
}
await AuthService.instance.loginByAlipay(authCode: authCode);
if (mounted) Navigator.pushReplacementNamed(context, '/main');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.alipayLoginFailed'))),
);
}
} finally {
if (mounted) setState(() => _alipayLoading = false);
}
}
/// 从 URL Query 格式字符串中提取参数值
String? _parseParam(String str, String key) {
final uri = Uri(query: str);
return uri.queryParameters[key];
}
// ── Google ─────────────────────────────────────────────────────────────────
Future<void> _onGoogleTap() async {
setState(() => _googleLoading = true);
try {
final account = await _googleSignIn.signIn();
if (account == null) return; // 用户取消
final auth = await account.authentication;
final idToken = auth.idToken;
if (idToken == null) throw Exception('未获取到 ID Token');
await AuthService.instance.loginByGoogle(idToken: idToken);
if (mounted) Navigator.pushReplacementNamed(context, '/main');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.googleLoginFailed'))),
);
}
} finally {
if (mounted) setState(() => _googleLoading = false);
}
}
// ── Apple仅 iOS────────────────────────────────────────────────────────
Future<void> _onAppleTap() async {
setState(() => _appleLoading = true);
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
final identityToken = credential.identityToken;
if (identityToken == null) throw Exception('未获取到 Identity Token');
// 拼接用户显示名Apple 首次登录时提供,之后为 null
final displayName = [
credential.givenName,
credential.familyName,
].where((s) => s != null && s.isNotEmpty).join(' ');
await AuthService.instance.loginByApple(
identityToken: identityToken,
displayName: displayName.isNotEmpty ? displayName : null,
);
if (mounted) Navigator.pushReplacementNamed(context, '/main');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t('welcome.appleLoginFailed'))),
);
}
} finally {
if (mounted) setState(() => _appleLoading = false);
}
}
// ── Build ─────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
final isIos = defaultTargetPlatform == TargetPlatform.iOS;
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const Spacer(flex: 2),
// Brand Logo
ClipRRect(
borderRadius: AppSpacing.borderRadiusXl,
child: Image.asset(
'assets/images/logo_icon.png',
width: 80,
height: 80,
),
),
const SizedBox(height: 12),
// Brand Name — 遵循 lockup 设计: "GEN" 深色 + "EX" 渐变
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'GEN',
style: AppTypography.displayLarge.copyWith(
color: const Color(0xFF1A103A),
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
),
ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [Color(0xFF9B8FFF), Color(0xFFB8ADFF)],
).createShader(bounds),
child: Text(
'EX',
style: AppTypography.displayLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
),
),
],
),
const SizedBox(height: 6),
// Slogan
Text(
context.t('welcome.slogan'),
style: AppTypography.bodyLarge.copyWith(
color: AppColors.textSecondary,
),
),
const Spacer(flex: 3),
// Phone Register
GenexButton(
label: context.t('welcome.phoneRegister'),
icon: Icons.phone_android_rounded,
onPressed: () {
Navigator.pushNamed(context, '/register');
},
),
const SizedBox(height: 12),
// Email Register
GenexButton(
label: context.t('welcome.emailRegister'),
icon: Icons.email_outlined,
variant: GenexButtonVariant.outline,
onPressed: () {
Navigator.pushNamed(context, '/register', arguments: {'isEmail': true});
},
),
const SizedBox(height: 24),
// Social Login Divider
Row(
children: [
const Expanded(child: Divider(color: AppColors.border)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(context.t('welcome.otherLogin'), style: AppTypography.caption),
),
const Expanded(child: Divider(color: AppColors.border)),
],
),
const SizedBox(height: 16),
// Social Login Buttons
// Android: 微信 + 支付宝 + Google
// iOS: 微信 + 支付宝 + Google + Apple
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_SocialLoginButton(
icon: Icons.wechat,
label: context.t('welcome.wechat'),
color: const Color(0xFF07C160),
loading: _wechatLoading,
onTap: _onWechatTap,
),
const SizedBox(width: 20),
_SocialLoginButton(
// 支付宝官方蓝色
icon: Icons.account_balance_wallet_outlined,
label: context.t('welcome.alipay'),
color: const Color(0xFF1677FF),
loading: _alipayLoading,
onTap: _onAlipayTap,
),
const SizedBox(width: 20),
_SocialLoginButton(
icon: Icons.g_mobiledata_rounded,
label: 'Google',
loading: _googleLoading,
onTap: _onGoogleTap,
),
if (isIos) ...[
const SizedBox(width: 20),
_SocialLoginButton(
icon: Icons.apple_rounded,
label: 'Apple',
loading: _appleLoading,
onTap: _onAppleTap,
),
],
],
),
const SizedBox(height: 32),
// Already have account
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(context.t('welcome.hasAccount'), style: AppTypography.bodyMedium.copyWith(
color: AppColors.textSecondary,
)),
GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/login');
},
child: Text(context.t('welcome.login'), style: AppTypography.labelMedium.copyWith(
color: AppColors.primary,
)),
),
],
),
const SizedBox(height: 16),
// Terms
Text(
context.t('welcome.agreement'),
style: AppTypography.caption.copyWith(fontSize: 10),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
),
),
),
);
}
}
class _SocialLoginButton extends StatelessWidget {
final IconData icon;
final String label;
final Color? color;
final VoidCallback onTap;
final bool loading;
const _SocialLoginButton({
required this.icon,
required this.label,
required this.onTap,
this.color,
this.loading = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: loading ? null : onTap,
child: Column(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: color != null ? color!.withValues(alpha: 0.1) : AppColors.gray50,
shape: BoxShape.circle,
border: Border.all(color: color?.withValues(alpha: 0.3) ?? AppColors.border),
),
child: loading
? Padding(
padding: const EdgeInsets.all(14),
child: CircularProgressIndicator(
strokeWidth: 2,
color: color ?? AppColors.textPrimary,
),
)
: Icon(icon, size: 28, color: color ?? AppColors.textPrimary),
),
const SizedBox(height: 6),
Text(label, style: AppTypography.caption),
],
),
);
}
}