From d4a7633f28fadb7b376df0a10c46d475ec680259 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 21 Jul 2026 15:22:47 +0800 Subject: [PATCH] feat: support Apple review guest flow --- assets/l10n/intl_ar.json | 2 + assets/l10n/intl_bn.json | 2 + assets/l10n/intl_en.json | 2 + assets/l10n/intl_fr.json | 2 + assets/l10n/intl_it.json | 2 + assets/l10n/intl_pt.json | 2 + assets/l10n/intl_tr.json | 4 +- ios/Runner/Info.plist | 4 -- lib/app_localizations.dart | 4 ++ lib/modules/auth/login_page.dart | 30 ++++++++++ lib/modules/index/index_page.dart | 47 +++++++++++----- lib/modules/splash/splash_page.dart | 5 ++ lib/services/audio/rtc_manager.dart | 9 +++ lib/services/auth/authentication_manager.dart | 55 +++++++++++++++++-- .../repositories/user_repository.dart | 6 +- .../sources/local/data_persistence.dart | 10 ++++ .../sources/local/user_manager.dart | 22 ++++++++ .../data_sources/sources/remote/net/api.dart | 22 ++++++-- .../repositories/sc_user_repository_impl.dart | 9 ++- 19 files changed, 208 insertions(+), 31 deletions(-) diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index 96a7eb7..c5e1c51 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -3,6 +3,8 @@ "or": "أو", "signInWithYourAccount": "تسجيل الدخول باستخدام حسابك", "signInWithApple": "تسجيل الدخول باستخدام آبل", + "continueAsGuest": "المتابعة كزائر", + "guestRoomLoginRequired": "سجّل الدخول لدخول الغرفة.", "loginRepresentsAgreementTo": "يعني تسجيل الدخول موافقتك على", "termsofService": "شروط الخدمة", "privaceyPolicy": "سياسة الخصوصية", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index 7d9de81..5f1aa2d 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -3,6 +3,8 @@ "or": "অথবা", "signInWithYourAccount": "আপনার অ্যাকাউন্ট দিয়ে লগইন করুন", "signInWithApple": "Apple দিয়ে লগইন করুন", + "continueAsGuest": "অতিথি হিসেবে চালিয়ে যান", + "guestRoomLoginRequired": "রুমে প্রবেশ করতে লগ ইন করুন।", "loginRepresentsAgreementTo": "লগইন করলে আপনি নিম্নলিখিতগুলি স্বীকার করছেন:", "termsofService": "সেবা শর্তাবলী", "privaceyPolicy": "গোপনীয়তা নীতি", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index cf948fc..667bcb3 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -3,6 +3,8 @@ "or": "Or", "signInWithYourAccount": "Sign in with your account", "signInWithApple": "Sign in with Apple", + "continueAsGuest": "Continue as Guest", + "guestRoomLoginRequired": "Sign in to enter a room.", "loginRepresentsAgreementTo": "Login represents agreement to", "termsofService": "Terms of service", "privaceyPolicy": "Privacy Policy", diff --git a/assets/l10n/intl_fr.json b/assets/l10n/intl_fr.json index e392371..9d3a0f5 100644 --- a/assets/l10n/intl_fr.json +++ b/assets/l10n/intl_fr.json @@ -3,6 +3,8 @@ "or": "Ou", "signInWithYourAccount": "Connectez-vous avec votre compte", "signInWithApple": "Connectez-vous avec Apple", + "continueAsGuest": "Continuer en tant qu’invité", + "guestRoomLoginRequired": "Connectez-vous pour entrer dans une salle.", "loginRepresentsAgreementTo": "La connexion représente l'accord de", "termsofService": "Conditions d'utilisation", "privaceyPolicy": "Politique de confidentialité", diff --git a/assets/l10n/intl_it.json b/assets/l10n/intl_it.json index 1c9f41a..c9e2737 100644 --- a/assets/l10n/intl_it.json +++ b/assets/l10n/intl_it.json @@ -3,6 +3,8 @@ "or": "Oppure", "signInWithYourAccount": "Accedi con il tuo account", "signInWithApple": "Accedi con Apple", + "continueAsGuest": "Continua come ospite", + "guestRoomLoginRequired": "Accedi per entrare in una stanza.", "loginRepresentsAgreementTo": "L'accesso rappresenta l'accordo", "termsofService": "Termini di servizio", "privaceyPolicy": "Informativa sulla privacy", diff --git a/assets/l10n/intl_pt.json b/assets/l10n/intl_pt.json index b4d79b9..ddd7523 100644 --- a/assets/l10n/intl_pt.json +++ b/assets/l10n/intl_pt.json @@ -3,6 +3,8 @@ "or": "Ou", "signInWithYourAccount": "Faça login com sua conta", "signInWithApple": "Faça login com a Apple", + "continueAsGuest": "Continuar como visitante", + "guestRoomLoginRequired": "Faça login para entrar em uma sala.", "loginRepresentsAgreementTo": "Login representa acordo para", "termsofService": "Termos de serviço", "privaceyPolicy": "Política de Privacidade", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index ee97df7..c996f5d 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -2,7 +2,9 @@ "signInWithGoogle": "Google ile Giriş Yap", "or": "Veya", "signInWithYourAccount": "Hesabınız ile Giriş Yap", - "signInWithApple": "Apple ile Giriş Yap", + "signInWithApple": "Apple ile Giriş Yap", + "continueAsGuest": "Misafir olarak devam et", + "guestRoomLoginRequired": "Bir odaya girmek için giriş yapın.", "loginRepresentsAgreementTo": "Giriş yapmak, aşağıdakilere kabul ettiğiniz anlamına gelir", "termsofService": "Hizmet Şartları", "privaceyPolicy": "Gizlilik Politikası", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3d176d0..366aaae 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -89,9 +89,5 @@ UIViewControllerBasedStatusBarAppearance - UIBackgroundModes - - audio - diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index 6c03225..ee41d9f 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -862,6 +862,10 @@ class SCAppLocalizations { String get signInWithApple => translate('signInWithApple'); + String get continueAsGuest => translate('continueAsGuest'); + + String get guestRoomLoginRequired => translate('guestRoomLoginRequired'); + String get deleteConversationTips => translate('deleteConversationTips'); String get loginRepresentsAgreementTo => diff --git a/lib/modules/auth/login_page.dart b/lib/modules/auth/login_page.dart index 6d71362..b86dabd 100644 --- a/lib/modules/auth/login_page.dart +++ b/lib/modules/auth/login_page.dart @@ -13,7 +13,9 @@ import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/services/auth/authentication_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/ui_kit/widgets/pop/pop_route.dart'; import 'package:yumi/modules/webview/webview_page.dart'; import 'package:yumi/modules/auth/login_route.dart'; @@ -247,6 +249,21 @@ class LoginPageState extends State with WidgetsBindingObserver { ), ), ), + SizedBox(height: 10.w), + SCDebounceWidget( + onTap: _continueAsGuest, + child: Container( + height: 42.w, + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.continueAsGuest, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + ), + ), + ), ], ), SizedBox(height: 8.w), @@ -432,4 +449,17 @@ class LoginPageState extends State with WidgetsBindingObserver { } authProvider?.authenticateUser(context, SCAuthType.APPLE.name); } + + Future _continueAsGuest() async { + if (!isAgreement) { + showPolicyConfirm(); + return; + } + // 游客不创建账号、不生成 token,只进入公开浏览能力。 + await AccountStorage().enterGuestMode(); + if (!mounted) { + return; + } + await SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); + } } diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart index 1a29c29..5839ec9 100644 --- a/lib/modules/index/index_page.dart +++ b/lib/modules/index/index_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/main.dart' show routeObserver; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/modules/home/index_home_page.dart'; @@ -63,32 +64,45 @@ class _SCIndexPageState extends State @override void initState() { super.initState(); + final isGuestMode = AccountStorage().isGuestMode; WidgetsBinding.instance.addObserver(this); _registerRewardSubscription = eventBus .on() .listen(_onRegisterRewardGranted); _initializePages(); - unawaited(_loadTaskClaimableCount()); + if (!isGuestMode) { + unawaited(_loadTaskClaimableCount()); + } generalProvider = Provider.of(context, listen: false); final rtcProvider = Provider.of(context, listen: false); - rtcProvider.initializeRealTimeCommunicationManager(context); - unawaited( - rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}), - ); - Provider.of(context, listen: false).init(context); - Provider.of( - context, - listen: false, - ).getUserIdentity(); - Provider.of(context, listen: false).balance(); - SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + if (!isGuestMode) { + // 实时通信、账户资料与心跳都属于登录能力,游客首页只加载公开内容。 + rtcProvider.initializeRealTimeCommunicationManager(context); + unawaited( + rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}), + ); + Provider.of(context, listen: false).init(context); + Provider.of( + context, + listen: false, + ).getUserIdentity(); + Provider.of( + context, + listen: false, + ).balance(); + SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + } DataPersistence.setLang(SCGlobalConfig.lang); OverlayManager().activate(); WidgetsBinding.instance.addPostFrameCallback((_) { WakelockPlus.enable(); - _showEntryDialogs(); + if (!isGuestMode) { + _showEntryDialogs(); + } }); - unawaited(rtcProvider.cleanupPersistedVoiceRoomSessionBeforeEntry()); + if (!isGuestMode) { + unawaited(rtcProvider.cleanupPersistedVoiceRoomSessionBeforeEntry()); + } } @override @@ -294,6 +308,11 @@ class _SCIndexPageState extends State } void _switchBottomTab(int index) { + if (AccountStorage().isGuestMode && index != 0) { + // 任务、消息和“我的”均为账号能力,游客点击时再引导登录。 + unawaited(SCNavigatorUtils.pushLoginIfNeeded(context)); + return; + } if (index == 1) { unawaited(_loadTaskClaimableCount()); } diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart index 0f38c69..2a35e6d 100644 --- a/lib/modules/splash/splash_page.dart +++ b/lib/modules/splash/splash_page.dart @@ -451,6 +451,11 @@ class _SplashPageState extends State { try { var user = AccountStorage().getCurrentUser(); var token = AccountStorage().getToken(); + if (AccountStorage().isGuestMode) { + // 游客无需恢复任何第三方会话,直接回到公开浏览首页。 + SCNavigatorUtils.push(context, SCRoutes.home, replace: true); + return; + } if (user != null && token.isNotEmpty) { final canContinue = await SCVersionUtils.checkReview(context: context); if (!mounted || !canContinue) { diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index f1fe40d..8867afb 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart'; @@ -2699,6 +2700,14 @@ class RealTimeCommunicationManager extends ChangeNotifier { RoomEntryPreviewData? previewData, }) async { SCFloatIchart().remove(); + if (AccountStorage().isGuestMode || AccountStorage().getToken().isEmpty) { + // 所有房间入口最终都会收敛到这里,确保深链、搜索和首页卡片同样受限。 + SCTts.show(SCAppLocalizations.of(context)!.guestRoomLoginRequired); + if (context.mounted) { + unawaited(SCNavigatorUtils.pushLoginIfNeeded(context)); + } + return; + } int nextTime = DateTime.now().millisecondsSinceEpoch; if (nextTime - startTime < 1000) { //频繁点击 diff --git a/lib/services/auth/authentication_manager.dart b/lib/services/auth/authentication_manager.dart index 47fd382..b4aefba 100644 --- a/lib/services/auth/authentication_manager.dart +++ b/lib/services/auth/authentication_manager.dart @@ -7,6 +7,8 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_google_auth_utils.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_version_utils.dart'; +import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; @@ -16,6 +18,7 @@ import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import '../../shared/data_sources/models/enum/sc_auth_type.dart'; +import '../../shared/data_sources/models/enum/sc_erro_code.dart'; class SocialChatAuthenticationManager extends ChangeNotifier { String authType = ""; @@ -105,15 +108,14 @@ class SocialChatAuthenticationManager extends ChangeNotifier { this.authType = authType; try { final appleCredential = await SignInWithApple.getAppleIDCredential( - scopes: [ - AppleIDAuthorizationScopes.fullName, // 请求姓名权限 - ], + // 审核要求 Apple 登录不能依赖姓名或邮箱授权。 + scopes: const [], ); uid = appleCredential.userIdentifier ?? ""; SCLoadingManager.show(); if (uid.isNotEmpty) { await DataPersistence.setPendingChannelAuth(authType, uid); - var user = await SCAccountRepository().loginForChannel(authType, uid); + final user = await _loginOrRegisterApple(authType, uid); if (!context.mounted) { SCLoadingManager.hide(); return; @@ -154,6 +156,51 @@ class SocialChatAuthenticationManager extends ChangeNotifier { } } + Future _loginOrRegisterApple( + String authType, + String openId, + ) async { + try { + return await SCAccountRepository().loginForChannel( + authType, + openId, + suppressRegistrationRedirect: true, + ); + } on DioException catch (error) { + if (!_isUserNotRegistered(error)) { + rethrow; + } + + // Apple 首登自动建立最小资料:昵称由服务端随机生成,未向 Apple 请求姓名或邮箱。 + final defaultBirthday = DateTime(2000, 1, 1); + final defaultProfile = SCUserProfileCmd( + userNickname: '', + userAvatar: '', + userSex: 0, + bornYear: defaultBirthday.year, + bornMonth: defaultBirthday.month, + bornDay: defaultBirthday.day, + age: DateTime.now().year - defaultBirthday.year, + countryCode: 'US', + countryName: 'United States', + ); + return SCAccountRepository().regist(authType, openId, defaultProfile); + } + } + + bool _isUserNotRegistered(DioException error) { + final data = error.response?.data; + if (data is! Map) { + return false; + } + final rawCode = data['errorCode'] ?? data['code']; + final code = + rawCode is num + ? rawCode.toInt() + : int.tryParse(rawCode?.toString() ?? ''); + return code == SCErroCode.userNotRegistered.code; + } + Future logoutUser() async { await SCGoogleAuthService.signOut(); await DataPersistence.clearPendingChannelAuth(); diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index c3d0692..1d3bf19 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -38,7 +38,11 @@ abstract class SocialChatUserRepository { Future loginForAccount(String account, String pwd); ///渠道登录谷歌 苹果 - Future loginForChannel(String authType, String openId); + Future loginForChannel( + String authType, + String openId, { + bool suppressRegistrationRedirect = false, + }); ///注册 Future regist( diff --git a/lib/shared/data_sources/sources/local/data_persistence.dart b/lib/shared/data_sources/sources/local/data_persistence.dart index 86c05c5..4c8e457 100644 --- a/lib/shared/data_sources/sources/local/data_persistence.dart +++ b/lib/shared/data_sources/sources/local/data_persistence.dart @@ -14,6 +14,7 @@ class DataPersistence { static const String _pendingChannelAuthTypeKey = "pending_channel_auth_type"; static const String _pendingChannelAuthOpenIdKey = "pending_channel_auth_open_id"; + static const String _guestModeKey = "guest_mode"; static SharedPreferences? _prefs; static bool _isInitialized = false; static Completer? _initializationCompleter; @@ -243,6 +244,15 @@ class DataPersistence { return getBool(_pendingFirstRegisterRoomGameEventKey); } + /// 游客态独立于登录 token 持久化,应用重启后仍可回到可浏览首页。 + static Future setGuestMode(bool value) async { + return setBool(_guestModeKey, value); + } + + static bool isGuestMode() { + return getBool(_guestModeKey); + } + static Future clearPendingFirstRegisterRoomGameEvent() async { return await remove(_pendingFirstRegisterRoomGameEventKey); } diff --git a/lib/shared/data_sources/sources/local/user_manager.dart b/lib/shared/data_sources/sources/local/user_manager.dart index 4b79300..cdbb465 100644 --- a/lib/shared/data_sources/sources/local/user_manager.dart +++ b/lib/shared/data_sources/sources/local/user_manager.dart @@ -30,6 +30,7 @@ class AccountStorage { String token = ""; SocialChatLoginRes? _currentUser; + bool? _guestMode; ///获取当前用户 SocialChatLoginRes? getCurrentUser() { @@ -50,6 +51,9 @@ class AccountStorage { void setCurrentUser(SocialChatLoginRes user) { _currentUser = user; token = _currentUser!.token ?? ""; + // 任意真实账号登录成功后必须退出游客态,避免后续功能仍被游客守卫拦截。 + _guestMode = false; + unawaited(DataPersistence.setGuestMode(false)); String userJson = jsonEncode(_currentUser?.toJson()); if (userJson.isNotEmpty) { unawaited(_persistSession(token: token, currentUserJson: userJson)); @@ -131,6 +135,7 @@ class AccountStorage { if (localToken.isNotEmpty && localUser != null) { token = localToken; _currentUser = localUser; + _guestMode = false; return; } @@ -153,6 +158,7 @@ class AccountStorage { token: session.token, currentUserJson: session.currentUserJson, ); + _guestMode = false; } void setToken(String tk) { @@ -163,10 +169,26 @@ class AccountStorage { void _cleanUser() { token = ""; _currentUser = null; + _guestMode = false; + unawaited(DataPersistence.setGuestMode(false)); unawaited(_clearPersistedSession()); unawaited(DataPersistence.clearPendingChannelAuth()); } + Future enterGuestMode() async { + token = ""; + _currentUser = null; + _guestMode = true; + await _clearPersistedSession(); + await DataPersistence.clearPendingChannelAuth(); + await DataPersistence.setGuestMode(true); + } + + bool get isGuestMode { + _guestMode ??= DataPersistence.isGuestMode(); + return _guestMode! && getToken().trim().isEmpty; + } + ///退出登录 void logout(BuildContext context) { _cleanUser(); diff --git a/lib/shared/data_sources/sources/remote/net/api.dart b/lib/shared/data_sources/sources/remote/net/api.dart index 1e23716..c6fd42d 100644 --- a/lib/shared/data_sources/sources/remote/net/api.dart +++ b/lib/shared/data_sources/sources/remote/net/api.dart @@ -108,6 +108,8 @@ class BaseNetworkClient { final Dio dio; static const String silentErrorToastKey = 'silentErrorToast'; static const String suppressAuthLogoutKey = 'suppressAuthLogout'; + static const String suppressRegistrationRedirectKey = + 'suppressRegistrationRedirect'; static const String baseUrlOverrideKey = 'baseUrlOverride'; BaseNetworkClient() : dio = Dio() { @@ -367,11 +369,21 @@ class BaseNetworkClient { } if (errorCode == SCErroCode.userNotRegistered.code) { - // 用户还没有注册:Google/Apple 登录后需要继续补充资料。 - SCTts.show("Please register an account first."); - final BuildContext? context = navigatorKey.currentContext; - if (context != null && context.mounted) { - SCNavigatorUtils.push(context, LoginRouter.editProfile, replace: false); + final suppressRegistrationRedirect = + e.requestOptions.extra[BaseNetworkClient + .suppressRegistrationRedirectKey] == + true; + if (!suppressRegistrationRedirect) { + // 需要人工补充资料的渠道仍沿用资料页;Apple 首登由认证层自动建档。 + SCTts.show("Please register an account first."); + final BuildContext? context = navigatorKey.currentContext; + if (context != null && context.mounted) { + SCNavigatorUtils.push( + context, + LoginRouter.editProfile, + replace: false, + ); + } } } else if (errorCode == SCErroCode.authUnauthorized.code) { // token过期 diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 0137aa1..3a5c40e 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -194,13 +194,18 @@ class SCAccountRepository implements SocialChatUserRepository { @override Future loginForChannel( String authType, - String openId, - ) async { + String openId, { + bool suppressRegistrationRedirect = false, + }) async { final data = {"authType": authType, "openId": openId}; data.addAll(await SCMobileLoginContext.loginBodyFields()); final result = await http.post( "e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44", data: data, + extra: + suppressRegistrationRedirect + ? const {BaseNetworkClient.suppressRegistrationRedirectKey: true} + : null, fromJson: (json) => SocialChatLoginRes.fromJson(json), ); return result;