优化:添加登录失败提示
This commit is contained in:
parent
439247bfff
commit
8455c3946b
78
lib/app.dart
78
lib/app.dart
@ -42,9 +42,9 @@ class _AppState extends State<App> 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<App> 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<App> with WidgetsBindingObserver {
|
||||
bottom: false,
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
FutureBuilder<void>(
|
||||
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<App> 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,7 +257,54 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
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),
|
||||
@ -265,6 +312,9 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware {
|
||||
ProfileScreen(isActive: widget.currentTab == NavTab.profile),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavBar(
|
||||
currentTab: widget.currentTab,
|
||||
onTabSelected: widget.onTabSelected,
|
||||
|
||||
@ -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: <String, dynamic>{
|
||||
'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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -49,9 +49,42 @@ class AuthService {
|
||||
|
||||
static Future<void>? _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<bool> isLoginComplete = ValueNotifier(false);
|
||||
|
||||
/// `fast_login` (and related) succeeded; when false but [isLoginComplete] is true, show the in-app retry strip.
|
||||
static final ValueNotifier<bool> startupSucceeded = ValueNotifier(false);
|
||||
|
||||
/// User-facing startup error copy; null when successful or not yet set.
|
||||
static final ValueNotifier<String?> 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<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
||||
|
||||
@ -191,22 +224,117 @@ 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<void> 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<void> init() async {
|
||||
if (_loginFuture != null) return _loginFuture!;
|
||||
_initGeneration++;
|
||||
final gen = _initGeneration;
|
||||
final completer = Completer<void>();
|
||||
_loginFuture = completer.future;
|
||||
_resetLoginFailTelemetry();
|
||||
|
||||
_logMsg('init: 开始快速登录');
|
||||
_logMsg('init: fast_login starting (gen=$gen)');
|
||||
const maxRetries = 3;
|
||||
const retryDelay = Duration(seconds: 2);
|
||||
|
||||
try {
|
||||
// 极短让步:避免个别机型刚起进程时网络栈未就绪;过长会拖慢冷启动
|
||||
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: 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<void> _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<void>.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
final deviceId = await _getDeviceId();
|
||||
_loginFailDeviceId = deviceId;
|
||||
_logMsg('init: deviceId=$deviceId');
|
||||
|
||||
final sign = _computeSign(deviceId);
|
||||
@ -220,13 +348,13 @@ class AuthService {
|
||||
await AuthTokenStore.restoreToApiClient();
|
||||
if (ApiClient.instance.proxy.userToken != null &&
|
||||
ApiClient.instance.proxy.userToken!.isNotEmpty) {
|
||||
_logMsg('init: 已用本地 token 注入请求头,本次 fast_login 将携带 knight');
|
||||
_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: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...');
|
||||
_logMsg('init: retry ${i + 1}, waiting ${retryDelay.inSeconds}s...');
|
||||
await Future<void>.delayed(retryDelay);
|
||||
}
|
||||
try {
|
||||
@ -238,15 +366,24 @@ class AuthService {
|
||||
);
|
||||
break;
|
||||
} catch (e) {
|
||||
_logMsg('init: 第 ${i + 1} 次请求失败: $e');
|
||||
_logMsg('init: attempt ${i + 1} failed: $e');
|
||||
if (i == maxRetries - 1) rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (res == null) return;
|
||||
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: 登录结果 code=${res.code} msg=${res.msg}');
|
||||
_logMsg('init: 登录响应 data=${res.data}');
|
||||
_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<String, dynamic>?;
|
||||
@ -254,9 +391,9 @@ class AuthService {
|
||||
if (token != null && token.isNotEmpty) {
|
||||
ApiClient.instance.setUserToken(token);
|
||||
await AuthTokenStore.write(token);
|
||||
_logMsg('init: 已设置 userToken 并写入本地');
|
||||
_logMsg('init: userToken set and persisted');
|
||||
} else {
|
||||
_logMsg('init: 响应中无 reevaluate (userToken),保留原本地 token(若有)');
|
||||
_logMsg('init: no reevaluate in response; keeping prior local token if any');
|
||||
}
|
||||
// equip = 服务端 firstRegister(见 docs/user_login.md),为 true/1 时上报注册事件
|
||||
final equipRaw = data?['equip'];
|
||||
@ -271,17 +408,17 @@ class AuthService {
|
||||
'adjust_register_date',
|
||||
DateTime.now().toIso8601String().substring(0, 10),
|
||||
);
|
||||
_logMsg('init: equip=true 首次注册,已上报 register');
|
||||
_logMsg('init: equip=true first register; register event sent');
|
||||
}
|
||||
final credits = data?['reveal'] as int?;
|
||||
if (credits != null) {
|
||||
UserState.setCredits(credits);
|
||||
_logMsg('init: 已同步积分 $credits');
|
||||
_logMsg('init: credits synced: $credits');
|
||||
}
|
||||
final uid = data?['asset'] as String?;
|
||||
if (uid != null && uid.isNotEmpty) {
|
||||
UserState.setUserId(uid);
|
||||
_logMsg('init: 已设置 userId');
|
||||
_logMsg('init: userId set');
|
||||
}
|
||||
final avatarUrl = data?['realm'] as String?;
|
||||
if (avatarUrl != null && avatarUrl.isNotEmpty) {
|
||||
@ -296,19 +433,19 @@ class AuthService {
|
||||
UserState.setNavigate(countryCode);
|
||||
}
|
||||
|
||||
// 3. 归因与 common_info:后台执行,不阻塞 loginComplete(避免服务端慢/挂起导致一直卡在启动遮罩)
|
||||
unawaited(_runPostLoginReferrerWork(uid!, deviceId));
|
||||
// Referrer + common_info in background; do not block loginComplete.
|
||||
if (uid != null && uid.isNotEmpty) {
|
||||
unawaited(_runPostLoginReferrerWork(uid, deviceId));
|
||||
}
|
||||
_markStartupSucceeded();
|
||||
} else {
|
||||
_logMsg('init: 登录失败');
|
||||
}
|
||||
} catch (e, st) {
|
||||
_logMsg('init: 异常 $e');
|
||||
_logMsg('init: 堆栈 $st');
|
||||
} finally {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete();
|
||||
isLoginComplete.value = true;
|
||||
}
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -379,7 +379,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
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<GalleryScreen> {
|
||||
if (refresh) _tasks = [];
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
_error = e.toString();
|
||||
_error = 'Something went wrong while loading your gallery. Please try again.';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,7 +284,7 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
@ -63,7 +63,7 @@ class _GenerationResultScreenState extends State<GenerationResultScreen> {
|
||||
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) {
|
||||
|
||||
@ -240,6 +240,8 @@ class _HomeScreenState extends State<HomeScreen> 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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -119,7 +119,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
);
|
||||
} 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<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
}
|
||||
} 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<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
}
|
||||
} 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();
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user