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;