diff --git a/lib/app.dart b/lib/app.dart index 2e91d0f..d9536e3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -42,9 +42,9 @@ class _AppState extends State with WidgetsBindingObserver { void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - // 冷启动时进程已在 resumed,didChangeAppLifecycleState 往往收不到「变为 resumed」,需首帧后再手动打一次 + // Cold start is often already resumed, so lifecycle may not emit resumed—fire once after first frame. WidgetsBinding.instance.addPostFrameCallback((_) { - // 去掉原生启动屏(黑底+Logo),进入 Flutter;之后由登录遮罩承接等待提示 + // Remove native splash; Flutter login overlay takes over if startup is still running. FlutterNativeSplash.remove(); _reportFacebookActivateApp('first_frame'); }); @@ -56,16 +56,16 @@ class _AppState extends State with WidgetsBindingObserver { super.dispose(); } - /// `AutoLogAppEventsEnabled=false` 时手动上报 Facebook **安装 + 应用激活**(activateApp) + /// With `AutoLogAppEventsEnabled=false`, manually report Facebook install + activateApp. void _reportFacebookActivateApp(String reason) { _fbAppEvents.activateApp().then((_) { if (FacebookConfig.debugLogs) { - _fbLog.d('activateApp(手动: $reason)'); + _fbLog.d('activateApp (manual: $reason)'); } }); } - /// 从后台回到前台时触发;冷启动依赖 initState 里的 addPostFrameCallback + /// Foreground resume; cold start also uses addPostFrameCallback in initState. @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { @@ -95,10 +95,10 @@ class _AppState extends State with WidgetsBindingObserver { bottom: false, child: child ?? const SizedBox.shrink(), ), - FutureBuilder( - future: AuthService.loginComplete, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { + ListenableBuilder( + listenable: AuthService.isLoginComplete, + builder: (context, _) { + if (AuthService.isLoginComplete.value) { return const SizedBox.shrink(); } return const _StartupLoginOverlay(); @@ -138,7 +138,7 @@ class _AppState extends State with WidgetsBindingObserver { } } -/// 启动阶段等待 [AuthService.loginComplete]:久等时提示网络/服务端问题,避免用户误以为「死机」。 +/// Full-screen wait until [AuthService.loginComplete]; after a delay, show a gentle network hint. class _StartupLoginOverlay extends StatefulWidget { const _StartupLoginOverlay(); @@ -181,7 +181,7 @@ class _StartupLoginOverlayState extends State<_StartupLoginOverlay> { if (_showNetworkHint) ...[ const SizedBox(height: 22), Text( - '网络较慢或暂时无法连接', + 'Still getting things ready', textAlign: TextAlign.center, style: TextStyle( color: AppColors.surface.withValues(alpha: 0.96), @@ -191,7 +191,7 @@ class _StartupLoginOverlayState extends State<_StartupLoginOverlay> { ), const SizedBox(height: 10), Text( - '请检查 Wi‑Fi 或移动数据,稍候片刻。若网络正常,可能是服务器繁忙,请过一会再试。', + 'This is slower than usual—your connection may be weak. Check Wi‑Fi or mobile data; loading will continue automatically. You can also fully close the app and open it again.', textAlign: TextAlign.center, style: TextStyle( color: AppColors.surface.withValues(alpha: 0.78), @@ -246,7 +246,7 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware { super.dispose(); } - /// 挂在 `/` 的 [PageRoute] 上:任意子页面 pop 后底层重新露出时触发(比 [HomeScreen] 内订阅更可靠) + /// Subscribed on `/` [PageRoute]: fires when a pushed route is popped and home is visible again. @override void didPopNext() { if (widget.currentTab == NavTab.home) { @@ -257,12 +257,62 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware { @override Widget build(BuildContext context) { return Scaffold( - body: IndexedStack( - index: widget.currentTab.index, + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - HomeScreen(isActive: widget.currentTab == NavTab.home), - GalleryScreen(isActive: widget.currentTab == NavTab.gallery), - ProfileScreen(isActive: widget.currentTab == NavTab.profile), + ListenableBuilder( + listenable: AuthService.startupUiListenable, + builder: (context, _) { + if (!AuthService.shouldShowStartupFailure) { + return const SizedBox.shrink(); + } + final msg = AuthService.startupFailureMessage.value ?? + 'We couldn’t finish loading. Check your network and tap Retry.'; + return Material( + color: const Color(0xFFFFF3E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 12, 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.wifi_off_rounded, + color: AppColors.primary.withValues(alpha: 0.85), + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + msg, + style: TextStyle( + color: Colors.brown.shade800, + fontSize: 14, + height: 1.35, + ), + ), + ), + TextButton( + onPressed: () async { + await AuthService.retryStartup(); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + }, + ), + Expanded( + child: IndexedStack( + index: widget.currentTab.index, + children: [ + HomeScreen(isActive: widget.currentTab == NavTab.home), + GalleryScreen(isActive: widget.currentTab == NavTab.gallery), + ProfileScreen(isActive: widget.currentTab == NavTab.profile), + ], + ), + ), ], ), bottomNavigationBar: BottomNavBar( diff --git a/lib/core/adjust/adjust_events.dart b/lib/core/adjust/adjust_events.dart index f94cc25..b48618a 100644 --- a/lib/core/adjust/adjust_events.dart +++ b/lib/core/adjust/adjust_events.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust_event.dart'; import 'package:facebook_app_events/facebook_app_events.dart'; +import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../config/facebook_config.dart'; +import '../user/user_state.dart'; /// 事件埋点:Adjust + Facebook App Events 双通道上报 /// Adjust 识别码见 docs/adjuest.md @@ -76,6 +78,44 @@ abstract final class AdjustEvents { unawaited(fn()); } + /// Facebook 自定义事件:冷启动 fast_login 失败(事件名按产品约定拼写为 LoginFaild)。 + /// + /// [apiMsg] 可为接口原文或异常信息,仅用于分析,不应对用户直接展示。 + static void trackLoginFaild({ + required String source, + int? apiCode, + String? apiMsg, + String? deviceId, + }) { + String truncate(String? s, int max) { + final t = (s ?? '').trim(); + if (t.length <= max) return t; + return '${t.substring(0, max)}…'; + } + + final serverCountry = UserState.navigate.value?.trim() ?? ''; + final localeCountry = + (PlatformDispatcher.instance.locale.countryCode ?? '').trim(); + + final msg = truncate(apiMsg, 500); + + _trackFb( + 'LoginFaild source=$source api_code=${apiCode ?? ''}', + () => _fb.logEvent( + name: 'LoginFaild', + parameters: { + 'source': source, + 'api_code': (apiCode ?? '').toString(), + 'api_message': msg, + 'device_id': deviceId ?? '', + 'platform': defaultTargetPlatform.name, + 'server_country': serverCountry, + 'locale_country': localeCountry, + }, + ), + ); + } + /// 购买档位(用户点击某档位购买时) static void trackTier(String eventToken) { _track(eventToken); diff --git a/lib/core/api/api_config.dart b/lib/core/api/api_config.dart index df53792..3dbba42 100644 --- a/lib/core/api/api_config.dart +++ b/lib/core/api/api_config.dart @@ -40,4 +40,7 @@ abstract final class ApiConfig { /// 单次 HTTP 请求超时(避免弱网/服务端无响应时永久卡住启动流程) static const Duration httpRequestTimeout = Duration(seconds: 20); + + /// Wall-clock cap for cold-start login (device id, attribution, fast_login, etc.); then dismiss overlay and show in-app retry. + static const Duration startupWallTimeout = Duration(seconds: 72); } diff --git a/lib/core/api/proxy_client.dart b/lib/core/api/proxy_client.dart index f1a6c60..4648160 100644 --- a/lib/core/api/proxy_client.dart +++ b/lib/core/api/proxy_client.dart @@ -256,7 +256,12 @@ class ProxyClient { final v2BodyEncoded = jsonEncode(v2Body); final logStr = - '========== 原始入参 ===========\npath: $path\nmethod: $method\nqueryParams: $paramsEncoded\nbody(sanctum): ${jsonEncode(sanctum)}'; + '========== request (pre-encrypt) ===========\n' + 'path: $path\n' + 'method: $method\n' + 'headers(V2 logical → encrypted into body): $headersEncoded\n' + 'queryParams: $paramsEncoded\n' + 'body(sanctum): ${jsonEncode(sanctum)}'; _log(logStr); final petSpeciesEnc = ApiCrypto.encrypt(path); @@ -283,12 +288,14 @@ class ProxyClient { final url = '$_baseUrl${ApiConfig.proxyPath}'; _log('真实请求URL: $url'); + const httpTransportHeaders = {'Content-Type': 'application/json'}; + _log('HTTP transport headers: ${jsonEncode(httpTransportHeaders)}'); http.Response response; try { response = await http .post( Uri.parse(url), - headers: {'Content-Type': 'application/json'}, + headers: httpTransportHeaders, body: jsonEncode(proxyBody), ) .timeout(ApiConfig.httpRequestTimeout); diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index e1e702e..77b7a81 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -49,9 +49,42 @@ class AuthService { static Future? _loginFuture; - /// 登录是否已完成,用于 UI 控制(如登录中隐藏页面加载指示器) + /// Bumped each [init] so a superseded session’s `finally` does not complete the wrong [Completer]. + static int _initGeneration = 0; + + /// Context for Facebook custom event `LoginFaild` (sent once in [init] `finally` on failure). + static String? _loginFailDeviceId; + static int? _loginFailApiCode; + static String? _loginFailApiMsg; + static String _loginFailSource = ''; + + static void _resetLoginFailTelemetry() { + _loginFailDeviceId = null; + _loginFailApiCode = null; + _loginFailApiMsg = null; + _loginFailSource = ''; + } + + /// Startup sequence finished (success or failure); drives overlay and list loading gates. static final ValueNotifier isLoginComplete = ValueNotifier(false); + /// `fast_login` (and related) succeeded; when false but [isLoginComplete] is true, show the in-app retry strip. + static final ValueNotifier startupSucceeded = ValueNotifier(false); + + /// User-facing startup error copy; null when successful or not yet set. + static final ValueNotifier startupFailureMessage = ValueNotifier(null); + + /// True when startup ended without success—show friendly UI instead of an endless spinner. + static bool get shouldShowStartupFailure => + isLoginComplete.value && !startupSucceeded.value; + + /// Merged listenable for overlay / home retry strip rebuilds. + static Listenable get startupUiListenable => Listenable.merge([ + isLoginComplete, + startupSucceeded, + startupFailureMessage, + ]); + /// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求 static Future get loginComplete => _loginFuture ?? Future.value(); @@ -191,124 +224,228 @@ class AuthService { } } - /// APP 启动时调用快速登录 - /// 启动时网络可能未就绪,会延迟后重试 + static void _markStartupFailed(String message) { + startupSucceeded.value = false; + startupFailureMessage.value = message; + } + + /// Never show raw [ApiResponse.msg] to users; log it only. + static String _friendlyHintForFailedLogin(ApiResponse res) { + if (res.code == -1) { + return 'Your current network temporarily does not support access. Please switch to another network and try again.'; + } + return 'We couldn’t sign you in just yet. Please try again in a moment, or tap Retry.'; + } + + static void _markStartupSucceeded() { + startupSucceeded.value = true; + startupFailureMessage.value = null; + } + + /// Retry cold-start login from the home banner (only after a finished, failed attempt). + static Future retryStartup() async { + if (!isLoginComplete.value) return; + if (startupSucceeded.value) return; + _loginFuture = null; + startupSucceeded.value = false; + startupFailureMessage.value = null; + isLoginComplete.value = false; + await init(); + } + + /// Cold-start fast login; retries on transient errors; whole flow is capped by [ApiConfig.startupWallTimeout]. static Future init() async { if (_loginFuture != null) return _loginFuture!; + _initGeneration++; + final gen = _initGeneration; final completer = Completer(); _loginFuture = completer.future; + _resetLoginFailTelemetry(); - _logMsg('init: 开始快速登录'); + _logMsg('init: fast_login starting (gen=$gen)'); const maxRetries = 3; const retryDelay = Duration(seconds: 2); try { - // 极短让步:避免个别机型刚起进程时网络栈未就绪;过长会拖慢冷启动 - await Future.delayed(const Duration(milliseconds: 400)); - - final deviceId = await _getDeviceId(); - _logMsg('init: deviceId=$deviceId'); - - final sign = _computeSign(deviceId); - _logMsg('init: sign=$sign'); - - final crest = await ReferrerService.getReferrer(); - if (crest != null && crest.isNotEmpty) { - _logMsg('init: crest(referrer)=$crest'); - } - - await AuthTokenStore.restoreToApiClient(); - if (ApiClient.instance.proxy.userToken != null && - ApiClient.instance.proxy.userToken!.isNotEmpty) { - _logMsg('init: 已用本地 token 注入请求头,本次 fast_login 将携带 knight'); - } - - ApiResponse? res; - for (var i = 0; i < maxRetries; i++) { - if (i > 0) { - _logMsg('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...'); - await Future.delayed(retryDelay); - } - try { - res = await UserApi.fastLogin( - origin: deviceId, - resolution: sign, - digest: crest ?? '', - crest: crest, - ); - break; - } catch (e) { - _logMsg('init: 第 ${i + 1} 次请求失败: $e'); - if (i == maxRetries - 1) rethrow; - } - } - - if (res == null) return; - - _logMsg('init: 登录结果 code=${res.code} msg=${res.msg}'); - _logMsg('init: 登录响应 data=${res.data}'); - - if (res.isSuccess && res.data != null) { - final data = res.data as Map?; - final token = data?['reevaluate'] as String?; - if (token != null && token.isNotEmpty) { - ApiClient.instance.setUserToken(token); - await AuthTokenStore.write(token); - _logMsg('init: 已设置 userToken 并写入本地'); - } else { - _logMsg('init: 响应中无 reevaluate (userToken),保留原本地 token(若有)'); - } - // equip = 服务端 firstRegister(见 docs/user_login.md),为 true/1 时上报注册事件 - final equipRaw = data?['equip']; - final equipFirstRegister = equipRaw == true || - equipRaw == 1 || - equipRaw == '1' || - (equipRaw is String && equipRaw.toLowerCase() == 'true'); - if (equipFirstRegister) { - AdjustEvents.trackRegister(); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString( - 'adjust_register_date', - DateTime.now().toIso8601String().substring(0, 10), - ); - _logMsg('init: equip=true 首次注册,已上报 register'); - } - final credits = data?['reveal'] as int?; - if (credits != null) { - UserState.setCredits(credits); - _logMsg('init: 已同步积分 $credits'); - } - final uid = data?['asset'] as String?; - if (uid != null && uid.isNotEmpty) { - UserState.setUserId(uid); - _logMsg('init: 已设置 userId'); - } - final avatarUrl = data?['realm'] as String?; - if (avatarUrl != null && avatarUrl.isNotEmpty) { - UserState.setAvatar(avatarUrl); - } - final name = data?['terminal'] as String?; - if (name != null && name.isNotEmpty) { - UserState.setUserName(name); - } - final countryCode = data?['navigate'] as String?; - if (countryCode != null) { - UserState.setNavigate(countryCode); - } - - // 3. 归因与 common_info:后台执行,不阻塞 loginComplete(避免服务端慢/挂起导致一直卡在启动遮罩) - unawaited(_runPostLoginReferrerWork(uid!, deviceId)); - } else { - _logMsg('init: 登录失败'); - } + await _initBody( + maxRetries: maxRetries, + retryDelay: retryDelay, + ).timeout(ApiConfig.startupWallTimeout); + } on TimeoutException catch (e, st) { + _logMsg('init: wall-clock timeout $e'); + _logMsg('init: stack $st'); + _loginFailSource = 'startup_timeout'; + _loginFailApiCode = -1; + _loginFailApiMsg = + 'Wall clock timeout after ${ApiConfig.startupWallTimeout.inSeconds}s'; + _markStartupFailed( + 'This is taking longer than usual—your network may be slow. Check your connection, tap Retry, or try again later.', + ); } catch (e, st) { - _logMsg('init: 异常 $e'); - _logMsg('init: 堆栈 $st'); - } finally { - if (!completer.isCompleted) { - completer.complete(); - isLoginComplete.value = true; + _logMsg('init: exception $e'); + _logMsg('init: stack $st'); + if (_loginFailSource.isEmpty) { + _loginFailSource = 'exception'; + _loginFailApiMsg = e.toString(); } + if (!startupSucceeded.value && + (startupFailureMessage.value == null || + startupFailureMessage.value!.isEmpty)) { + _markStartupFailed( + 'Something went wrong and we couldn’t reach the service. Check your network and tap Retry, or try again later.', + ); + } + } finally { + if (gen != _initGeneration) { + _logMsg('init: gen=$gen superseded; skipping completer'); + } else { + if (!startupSucceeded.value && + (startupFailureMessage.value == null || + startupFailureMessage.value!.trim().isEmpty)) { + _markStartupFailed( + 'We couldn’t finish loading. Check your network, tap Retry, or open the app again in a little while.', + ); + } + if (!completer.isCompleted) { + completer.complete(); + isLoginComplete.value = true; + } + if (!startupSucceeded.value) { + if (_loginFailSource.isEmpty) { + _loginFailSource = 'unknown'; + } + AdjustEvents.trackLoginFaild( + source: _loginFailSource, + apiCode: _loginFailApiCode, + apiMsg: _loginFailApiMsg, + deviceId: _loginFailDeviceId, + ); + } + _resetLoginFailTelemetry(); + } + } + } + + static Future _initBody({ + required int maxRetries, + required Duration retryDelay, + }) async { + // Short yield so the network stack can settle on some devices; keep small for cold start. + await Future.delayed(const Duration(milliseconds: 400)); + + final deviceId = await _getDeviceId(); + _loginFailDeviceId = deviceId; + _logMsg('init: deviceId=$deviceId'); + + final sign = _computeSign(deviceId); + _logMsg('init: sign=$sign'); + + final crest = await ReferrerService.getReferrer(); + if (crest != null && crest.isNotEmpty) { + _logMsg('init: crest(referrer)=$crest'); + } + + await AuthTokenStore.restoreToApiClient(); + if (ApiClient.instance.proxy.userToken != null && + ApiClient.instance.proxy.userToken!.isNotEmpty) { + _logMsg('init: restored local token; fast_login will send knight if present'); + } + + ApiResponse? res; + for (var i = 0; i < maxRetries; i++) { + if (i > 0) { + _logMsg('init: retry ${i + 1}, waiting ${retryDelay.inSeconds}s...'); + await Future.delayed(retryDelay); + } + try { + res = await UserApi.fastLogin( + origin: deviceId, + resolution: sign, + digest: crest ?? '', + crest: crest, + ); + break; + } catch (e) { + _logMsg('init: attempt ${i + 1} failed: $e'); + if (i == maxRetries - 1) rethrow; + } + } + + if (res == null) { + _loginFailSource = 'no_response'; + _loginFailApiCode = null; + _loginFailApiMsg = + 'No ApiResponse after fast_login attempts (or error before response)'; + _markStartupFailed( + 'We couldn’t reach the server. Check your network and tap Retry, or try again shortly.', + ); + return; + } + + _logMsg('init: fast_login result code=${res.code} msg=${res.msg}'); + _logMsg('init: fast_login data=${res.data}'); + + if (res.isSuccess && res.data != null) { + final data = res.data as Map?; + final token = data?['reevaluate'] as String?; + if (token != null && token.isNotEmpty) { + ApiClient.instance.setUserToken(token); + await AuthTokenStore.write(token); + _logMsg('init: userToken set and persisted'); + } else { + _logMsg('init: no reevaluate in response; keeping prior local token if any'); + } + // equip = 服务端 firstRegister(见 docs/user_login.md),为 true/1 时上报注册事件 + final equipRaw = data?['equip']; + final equipFirstRegister = equipRaw == true || + equipRaw == 1 || + equipRaw == '1' || + (equipRaw is String && equipRaw.toLowerCase() == 'true'); + if (equipFirstRegister) { + AdjustEvents.trackRegister(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + 'adjust_register_date', + DateTime.now().toIso8601String().substring(0, 10), + ); + _logMsg('init: equip=true first register; register event sent'); + } + final credits = data?['reveal'] as int?; + if (credits != null) { + UserState.setCredits(credits); + _logMsg('init: credits synced: $credits'); + } + final uid = data?['asset'] as String?; + if (uid != null && uid.isNotEmpty) { + UserState.setUserId(uid); + _logMsg('init: userId set'); + } + final avatarUrl = data?['realm'] as String?; + if (avatarUrl != null && avatarUrl.isNotEmpty) { + UserState.setAvatar(avatarUrl); + } + final name = data?['terminal'] as String?; + if (name != null && name.isNotEmpty) { + UserState.setUserName(name); + } + final countryCode = data?['navigate'] as String?; + if (countryCode != null) { + UserState.setNavigate(countryCode); + } + + // Referrer + common_info in background; do not block loginComplete. + if (uid != null && uid.isNotEmpty) { + unawaited(_runPostLoginReferrerWork(uid, deviceId)); + } + _markStartupSucceeded(); + } else { + _logMsg( + 'init: fast_login failed (user-facing copy sanitized) code=${res.code} msg=${res.msg}', + ); + _loginFailSource = 'fast_login_api'; + _loginFailApiCode = res.code; + _loginFailApiMsg = res.msg; + _markStartupFailed(_friendlyHintForFailedLogin(res)); } } diff --git a/lib/features/gallery/gallery_screen.dart b/lib/features/gallery/gallery_screen.dart index bd3ad63..16fe50e 100644 --- a/lib/features/gallery/gallery_screen.dart +++ b/lib/features/gallery/gallery_screen.dart @@ -379,7 +379,7 @@ class _GalleryScreenState extends State { if (refresh) _tasks = []; _loading = false; _loadingMore = false; - _error = res.msg.isNotEmpty ? res.msg : 'Failed to load'; + _error = 'Unable to load your gallery right now. Please check your connection and try again.'; }); } } catch (e) { @@ -388,7 +388,7 @@ class _GalleryScreenState extends State { if (refresh) _tasks = []; _loading = false; _loadingMore = false; - _error = e.toString(); + _error = 'Something went wrong while loading your gallery. Please try again.'; }); } } diff --git a/lib/features/generate_video/generate_video_screen.dart b/lib/features/generate_video/generate_video_screen.dart index 5f7a1bd..f13924a 100644 --- a/lib/features/generate_video/generate_video_screen.dart +++ b/lib/features/generate_video/generate_video_screen.dart @@ -284,7 +284,7 @@ class _GenerateVideoScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Generation failed: ${e.toString().replaceAll('Exception: ', '')}'), + 'We could not start generation right now. Please try again.'), behavior: SnackBarBehavior.floating, ), ); diff --git a/lib/features/generate_video/generation_result_screen.dart b/lib/features/generate_video/generation_result_screen.dart index 7bd5985..7015052 100644 --- a/lib/features/generate_video/generation_result_screen.dart +++ b/lib/features/generate_video/generation_result_screen.dart @@ -63,7 +63,7 @@ class _GenerationResultScreenState extends State { if (mounted) { setState(() { _videoLoading = false; - _videoLoadError = e.toString(); + _videoLoadError = 'Video preview is temporarily unavailable. Please try again later.'; }); } } @@ -519,7 +519,7 @@ class _ReportDialogState extends State<_ReportDialog> { } } catch (e) { if (mounted) { - setState(() => _errorText = e.toString().replaceAll('Exception: ', '')); + setState(() => _errorText = 'Unable to submit your report right now. Please try again.'); } } finally { if (mounted) { diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index 119015b..9a5f14d 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -240,6 +240,8 @@ class _HomeScreenState extends State with WidgetsBindingObserver { /// 登录未完成时不显示页面加载指示器,由登录遮罩负责 bool get _isListLoading { if (!AuthService.isLoginComplete.value) return false; + // After failed fast_login, extConfig never arrives so items stay null; without this, list loading spins forever. + if (AuthService.shouldShowStartupFailure) return false; if (_showVideoMenu) { if (_categoriesLoading) return true; if (_selectedCategory?.id == kExtCategoryId) { diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index 818e08b..025c255 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -422,11 +422,11 @@ class _DeleteAccountDialogState extends State<_DeleteAccountDialog> { ); } } else { - setState(() => _errorText = res.msg.isNotEmpty ? res.msg : 'Delete failed'); + setState(() => _errorText = 'Unable to delete your account right now. Please try again.'); } } catch (e) { if (mounted) { - setState(() => _errorText = e.toString().replaceAll('Exception: ', '')); + setState(() => _errorText = 'Something went wrong while deleting your account. Please try again.'); } } finally { if (mounted) { diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart index 67484e4..6121d1a 100644 --- a/lib/features/recharge/recharge_screen.dart +++ b/lib/features/recharge/recharge_screen.dart @@ -119,7 +119,7 @@ class _RechargeScreenState extends State setState(() { _activities = []; _loadingTiers = false; - _tierError = res.msg.isNotEmpty ? res.msg : 'Failed to load'; + _tierError = 'Unable to load plans right now. Please check your connection and try again.'; }); } } catch (e) { @@ -127,7 +127,7 @@ class _RechargeScreenState extends State setState(() { _activities = []; _loadingTiers = false; - _tierError = e.toString(); + _tierError = 'Something went wrong while loading plans. Please try again.'; }); } } @@ -185,9 +185,7 @@ class _RechargeScreenState extends State if (!methodsRes.isSuccess || methodsRes.data == null) { _showSnackBar( context, - methodsRes.msg.isNotEmpty - ? methodsRes.msg - : 'Failed to load payment methods', + 'Unable to load payment methods right now. Please try again shortly.', isError: true, ); AdjustEvents.trackPaymentFailed(); @@ -239,7 +237,7 @@ class _RechargeScreenState extends State ); } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); + _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -271,7 +269,7 @@ class _RechargeScreenState extends State if (!createRes.isSuccess) { _showSnackBar( context, - createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order', + 'Unable to create your order right now. Please try again.', isError: true, ); AdjustEvents.trackPaymentFailed(); @@ -342,7 +340,7 @@ class _RechargeScreenState extends State } } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); + _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -435,7 +433,7 @@ class _RechargeScreenState extends State if (!createRes.isSuccess) { _showSnackBar( context, - createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order', + 'Unable to create your order right now. Please try again.', isError: true, ); AdjustEvents.trackPaymentFailed(); @@ -449,7 +447,7 @@ class _RechargeScreenState extends State serverOrderId: orderId, userId: userId); } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); + _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -527,7 +525,7 @@ class _RechargeScreenState extends State } } catch (e) { if (mounted) { - _showSnackBar(context, 'Google Pay error: ${e.toString()}', + _showSnackBar(context, 'Google Pay is temporarily unavailable. Please try again.', isError: true); } AdjustEvents.trackPaymentFailed(); diff --git a/lib/main.dart b/lib/main.dart index 3b64848..b3777b9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await ensureDeviceMemoryProfileInitialized(); _initAdjust(); - // 勿在此 await ReferrerService.init():会阻塞首帧(Adjust 最长可达十余秒)。[AuthService.init] 内已 await getReferrer() + // Do not await ReferrerService.init() here—it can block first frame (Adjust may take many seconds). AuthService.init awaits getReferrer(). SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: AppColors.surface, @@ -34,8 +34,11 @@ void main() async { GooglePlayPurchaseService.startPendingPurchaseListener(); } // 登录完成后执行谷歌支付补单(未核销订单上报服务端并 completePurchase) - AuthService.loginComplete - .then((_) => GooglePlayPurchaseService.runOrderRecovery()); + AuthService.loginComplete.then((_) { + if (AuthService.startupSucceeded.value) { + GooglePlayPurchaseService.runOrderRecovery(); + } + }); } void _initAdjust() {