feat: support Apple review guest flow

This commit is contained in:
zhx 2026-07-21 15:22:47 +08:00
parent 7baa3b48ca
commit d4a7633f28
19 changed files with 208 additions and 31 deletions

View File

@ -3,6 +3,8 @@
"or": "أو",
"signInWithYourAccount": "تسجيل الدخول باستخدام حسابك",
"signInWithApple": "تسجيل الدخول باستخدام آبل",
"continueAsGuest": "المتابعة كزائر",
"guestRoomLoginRequired": "سجّل الدخول لدخول الغرفة.",
"loginRepresentsAgreementTo": "يعني تسجيل الدخول موافقتك على",
"termsofService": "شروط الخدمة",
"privaceyPolicy": "سياسة الخصوصية",

View File

@ -3,6 +3,8 @@
"or": "অথবা",
"signInWithYourAccount": "আপনার অ্যাকাউন্ট দিয়ে লগইন করুন",
"signInWithApple": "Apple দিয়ে লগইন করুন",
"continueAsGuest": "অতিথি হিসেবে চালিয়ে যান",
"guestRoomLoginRequired": "রুমে প্রবেশ করতে লগ ইন করুন।",
"loginRepresentsAgreementTo": "লগইন করলে আপনি নিম্নলিখিতগুলি স্বীকার করছেন:",
"termsofService": "সেবা শর্তাবলী",
"privaceyPolicy": "গোপনীয়তা নীতি",

View File

@ -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",

View File

@ -3,6 +3,8 @@
"or": "Ou",
"signInWithYourAccount": "Connectez-vous avec votre compte",
"signInWithApple": "Connectez-vous avec Apple",
"continueAsGuest": "Continuer en tant quinvité",
"guestRoomLoginRequired": "Connectez-vous pour entrer dans une salle.",
"loginRepresentsAgreementTo": "La connexion représente l'accord de",
"termsofService": "Conditions d'utilisation",
"privaceyPolicy": "Politique de confidentialité",

View File

@ -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",

View File

@ -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",

View File

@ -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ı",

View File

@ -89,9 +89,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@ -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 =>

View File

@ -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<LoginPage> 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<LoginPage> with WidgetsBindingObserver {
}
authProvider?.authenticateUser(context, SCAuthType.APPLE.name);
}
Future<void> _continueAsGuest() async {
if (!isAgreement) {
showPolicyConfirm();
return;
}
// token
await AccountStorage().enterGuestMode();
if (!mounted) {
return;
}
await SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true);
}
}

View File

@ -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<SCIndexPage>
@override
void initState() {
super.initState();
final isGuestMode = AccountStorage().isGuestMode;
WidgetsBinding.instance.addObserver(this);
_registerRewardSubscription = eventBus
.on<RegisterRewardGrantedEvent>()
.listen(_onRegisterRewardGranted);
_initializePages();
unawaited(_loadTaskClaimableCount());
if (!isGuestMode) {
unawaited(_loadTaskClaimableCount());
}
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.initializeRealTimeCommunicationManager(context);
unawaited(
rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}),
);
Provider.of<RtmProvider>(context, listen: false).init(context);
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).getUserIdentity();
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
if (!isGuestMode) {
//
rtcProvider.initializeRealTimeCommunicationManager(context);
unawaited(
rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}),
);
Provider.of<RtmProvider>(context, listen: false).init(context);
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).getUserIdentity();
Provider.of<SocialChatUserProfileManager>(
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<SCIndexPage>
}
void _switchBottomTab(int index) {
if (AccountStorage().isGuestMode && index != 0) {
//
unawaited(SCNavigatorUtils.pushLoginIfNeeded(context));
return;
}
if (index == 1) {
unawaited(_loadTaskClaimableCount());
}

View File

@ -451,6 +451,11 @@ class _SplashPageState extends State<SplashPage> {
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) {

View File

@ -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) {
//

View File

@ -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<SocialChatLoginRes> _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<void> logoutUser() async {
await SCGoogleAuthService.signOut();
await DataPersistence.clearPendingChannelAuth();

View File

@ -38,7 +38,11 @@ abstract class SocialChatUserRepository {
Future<SocialChatLoginRes> loginForAccount(String account, String pwd);
///
Future<SocialChatLoginRes> loginForChannel(String authType, String openId);
Future<SocialChatLoginRes> loginForChannel(
String authType,
String openId, {
bool suppressRegistrationRedirect = false,
});
///
Future<SocialChatLoginRes> regist(

View File

@ -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<void>? _initializationCompleter;
@ -243,6 +244,15 @@ class DataPersistence {
return getBool(_pendingFirstRegisterRoomGameEventKey);
}
/// token
static Future<bool> setGuestMode(bool value) async {
return setBool(_guestModeKey, value);
}
static bool isGuestMode() {
return getBool(_guestModeKey);
}
static Future<bool> clearPendingFirstRegisterRoomGameEvent() async {
return await remove(_pendingFirstRegisterRoomGameEventKey);
}

View File

@ -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<void> 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();

View File

@ -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过期

View File

@ -194,13 +194,18 @@ class SCAccountRepository implements SocialChatUserRepository {
@override
Future<SocialChatLoginRes> loginForChannel(
String authType,
String openId,
) async {
String openId, {
bool suppressRegistrationRedirect = false,
}) async {
final data = <String, dynamic>{"authType": authType, "openId": openId};
data.addAll(await SCMobileLoginContext.loginBodyFields());
final result = await http.post<SocialChatLoginRes>(
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
data: data,
extra:
suppressRegistrationRedirect
? const {BaseNetworkClient.suppressRegistrationRedirectKey: true}
: null,
fromJson: (json) => SocialChatLoginRes.fromJson(json),
);
return result;