优化:添加登录失败提示

This commit is contained in:
ivan 2026-04-23 10:57:46 +08:00
parent 439247bfff
commit 8455c3946b
12 changed files with 389 additions and 149 deletions

View File

@ -42,9 +42,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// resumeddidChangeAppLifecycleState resumed // Cold start is often already resumed, so lifecycle may not emit resumedfire once after first frame.
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// +Logo Flutter // Remove native splash; Flutter login overlay takes over if startup is still running.
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
_reportFacebookActivateApp('first_frame'); _reportFacebookActivateApp('first_frame');
}); });
@ -56,16 +56,16 @@ class _AppState extends State<App> with WidgetsBindingObserver {
super.dispose(); super.dispose();
} }
/// `AutoLogAppEventsEnabled=false` Facebook ** + **activateApp /// With `AutoLogAppEventsEnabled=false`, manually report Facebook install + activateApp.
void _reportFacebookActivateApp(String reason) { void _reportFacebookActivateApp(String reason) {
_fbAppEvents.activateApp().then((_) { _fbAppEvents.activateApp().then((_) {
if (FacebookConfig.debugLogs) { if (FacebookConfig.debugLogs) {
_fbLog.d('activateApp(手动: $reason'); _fbLog.d('activateApp (manual: $reason)');
} }
}); });
} }
/// initState addPostFrameCallback /// Foreground resume; cold start also uses addPostFrameCallback in initState.
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
@ -95,10 +95,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
bottom: false, bottom: false,
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
), ),
FutureBuilder<void>( ListenableBuilder(
future: AuthService.loginComplete, listenable: AuthService.isLoginComplete,
builder: (context, snapshot) { builder: (context, _) {
if (snapshot.connectionState == ConnectionState.done) { if (AuthService.isLoginComplete.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return const _StartupLoginOverlay(); 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 { class _StartupLoginOverlay extends StatefulWidget {
const _StartupLoginOverlay(); const _StartupLoginOverlay();
@ -181,7 +181,7 @@ class _StartupLoginOverlayState extends State<_StartupLoginOverlay> {
if (_showNetworkHint) ...[ if (_showNetworkHint) ...[
const SizedBox(height: 22), const SizedBox(height: 22),
Text( Text(
'网络较慢或暂时无法连接', 'Still getting things ready',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: AppColors.surface.withValues(alpha: 0.96), color: AppColors.surface.withValues(alpha: 0.96),
@ -191,7 +191,7 @@ class _StartupLoginOverlayState extends State<_StartupLoginOverlay> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'请检查 WiFi 或移动数据,稍候片刻。若网络正常,可能是服务器繁忙,请过一会再试。', 'This is slower than usual—your connection may be weak. Check WiFi or mobile data; loading will continue automatically. You can also fully close the app and open it again.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: AppColors.surface.withValues(alpha: 0.78), color: AppColors.surface.withValues(alpha: 0.78),
@ -246,7 +246,7 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware {
super.dispose(); super.dispose();
} }
/// `/` [PageRoute] pop [HomeScreen] /// Subscribed on `/` [PageRoute]: fires when a pushed route is popped and home is visible again.
@override @override
void didPopNext() { void didPopNext() {
if (widget.currentTab == NavTab.home) { if (widget.currentTab == NavTab.home) {
@ -257,7 +257,54 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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 couldnt 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, index: widget.currentTab.index,
children: [ children: [
HomeScreen(isActive: widget.currentTab == NavTab.home), HomeScreen(isActive: widget.currentTab == NavTab.home),
@ -265,6 +312,9 @@ class _MainScaffoldState extends State<_MainScaffold> with RouteAware {
ProfileScreen(isActive: widget.currentTab == NavTab.profile), ProfileScreen(isActive: widget.currentTab == NavTab.profile),
], ],
), ),
),
],
),
bottomNavigationBar: BottomNavBar( bottomNavigationBar: BottomNavBar(
currentTab: widget.currentTab, currentTab: widget.currentTab,
onTabSelected: widget.onTabSelected, onTabSelected: widget.onTabSelected,

View File

@ -3,10 +3,12 @@ import 'dart:async';
import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_event.dart'; import 'package:adjust_sdk/adjust_event.dart';
import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../config/facebook_config.dart'; import '../config/facebook_config.dart';
import '../user/user_state.dart';
/// Adjust + Facebook App Events /// Adjust + Facebook App Events
/// Adjust docs/adjuest.md /// Adjust docs/adjuest.md
@ -76,6 +78,44 @@ abstract final class AdjustEvents {
unawaited(fn()); 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) { static void trackTier(String eventToken) {
_track(eventToken); _track(eventToken);

View File

@ -40,4 +40,7 @@ abstract final class ApiConfig {
/// HTTP / /// HTTP /
static const Duration httpRequestTimeout = Duration(seconds: 20); 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);
} }

View File

@ -256,7 +256,12 @@ class ProxyClient {
final v2BodyEncoded = jsonEncode(v2Body); final v2BodyEncoded = jsonEncode(v2Body);
final logStr = 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); _log(logStr);
final petSpeciesEnc = ApiCrypto.encrypt(path); final petSpeciesEnc = ApiCrypto.encrypt(path);
@ -283,12 +288,14 @@ class ProxyClient {
final url = '$_baseUrl${ApiConfig.proxyPath}'; final url = '$_baseUrl${ApiConfig.proxyPath}';
_log('真实请求URL: $url'); _log('真实请求URL: $url');
const httpTransportHeaders = {'Content-Type': 'application/json'};
_log('HTTP transport headers: ${jsonEncode(httpTransportHeaders)}');
http.Response response; http.Response response;
try { try {
response = await http response = await http
.post( .post(
Uri.parse(url), Uri.parse(url),
headers: {'Content-Type': 'application/json'}, headers: httpTransportHeaders,
body: jsonEncode(proxyBody), body: jsonEncode(proxyBody),
) )
.timeout(ApiConfig.httpRequestTimeout); .timeout(ApiConfig.httpRequestTimeout);

View File

@ -49,9 +49,42 @@ class AuthService {
static Future<void>? _loginFuture; static Future<void>? _loginFuture;
/// UI /// Bumped each [init] so a superseded sessions `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); 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 successshow 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 /// Future await Future
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value(); 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 couldnt 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 { static Future<void> init() async {
if (_loginFuture != null) return _loginFuture!; if (_loginFuture != null) return _loginFuture!;
_initGeneration++;
final gen = _initGeneration;
final completer = Completer<void>(); final completer = Completer<void>();
_loginFuture = completer.future; _loginFuture = completer.future;
_resetLoginFailTelemetry();
_logMsg('init: 开始快速登录'); _logMsg('init: fast_login starting (gen=$gen)');
const maxRetries = 3; const maxRetries = 3;
const retryDelay = Duration(seconds: 2); const retryDelay = Duration(seconds: 2);
try { 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 couldnt 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 couldnt 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)); await Future<void>.delayed(const Duration(milliseconds: 400));
final deviceId = await _getDeviceId(); final deviceId = await _getDeviceId();
_loginFailDeviceId = deviceId;
_logMsg('init: deviceId=$deviceId'); _logMsg('init: deviceId=$deviceId');
final sign = _computeSign(deviceId); final sign = _computeSign(deviceId);
@ -220,13 +348,13 @@ class AuthService {
await AuthTokenStore.restoreToApiClient(); await AuthTokenStore.restoreToApiClient();
if (ApiClient.instance.proxy.userToken != null && if (ApiClient.instance.proxy.userToken != null &&
ApiClient.instance.proxy.userToken!.isNotEmpty) { 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; ApiResponse? res;
for (var i = 0; i < maxRetries; i++) { for (var i = 0; i < maxRetries; i++) {
if (i > 0) { if (i > 0) {
_logMsg('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...'); _logMsg('init: retry ${i + 1}, waiting ${retryDelay.inSeconds}s...');
await Future<void>.delayed(retryDelay); await Future<void>.delayed(retryDelay);
} }
try { try {
@ -238,15 +366,24 @@ class AuthService {
); );
break; break;
} catch (e) { } catch (e) {
_logMsg('init: 第 ${i + 1} 次请求失败: $e'); _logMsg('init: attempt ${i + 1} failed: $e');
if (i == maxRetries - 1) rethrow; 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 couldnt reach the server. Check your network and tap Retry, or try again shortly.',
);
return;
}
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}'); _logMsg('init: fast_login result code=${res.code} msg=${res.msg}');
_logMsg('init: 登录响应 data=${res.data}'); _logMsg('init: fast_login data=${res.data}');
if (res.isSuccess && res.data != null) { if (res.isSuccess && res.data != null) {
final data = res.data as Map<String, dynamic>?; final data = res.data as Map<String, dynamic>?;
@ -254,9 +391,9 @@ class AuthService {
if (token != null && token.isNotEmpty) { if (token != null && token.isNotEmpty) {
ApiClient.instance.setUserToken(token); ApiClient.instance.setUserToken(token);
await AuthTokenStore.write(token); await AuthTokenStore.write(token);
_logMsg('init: 已设置 userToken 并写入本地'); _logMsg('init: userToken set and persisted');
} else { } 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 // equip = firstRegister docs/user_login.md true/1
final equipRaw = data?['equip']; final equipRaw = data?['equip'];
@ -271,17 +408,17 @@ class AuthService {
'adjust_register_date', 'adjust_register_date',
DateTime.now().toIso8601String().substring(0, 10), 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?; final credits = data?['reveal'] as int?;
if (credits != null) { if (credits != null) {
UserState.setCredits(credits); UserState.setCredits(credits);
_logMsg('init: 已同步积分 $credits'); _logMsg('init: credits synced: $credits');
} }
final uid = data?['asset'] as String?; final uid = data?['asset'] as String?;
if (uid != null && uid.isNotEmpty) { if (uid != null && uid.isNotEmpty) {
UserState.setUserId(uid); UserState.setUserId(uid);
_logMsg('init: 已设置 userId'); _logMsg('init: userId set');
} }
final avatarUrl = data?['realm'] as String?; final avatarUrl = data?['realm'] as String?;
if (avatarUrl != null && avatarUrl.isNotEmpty) { if (avatarUrl != null && avatarUrl.isNotEmpty) {
@ -296,19 +433,19 @@ class AuthService {
UserState.setNavigate(countryCode); UserState.setNavigate(countryCode);
} }
// 3. common_info loginComplete/ // Referrer + common_info in background; do not block loginComplete.
unawaited(_runPostLoginReferrerWork(uid!, deviceId)); if (uid != null && uid.isNotEmpty) {
unawaited(_runPostLoginReferrerWork(uid, deviceId));
}
_markStartupSucceeded();
} else { } else {
_logMsg('init: 登录失败'); _logMsg(
} 'init: fast_login failed (user-facing copy sanitized) code=${res.code} msg=${res.msg}',
} catch (e, st) { );
_logMsg('init: 异常 $e'); _loginFailSource = 'fast_login_api';
_logMsg('init: 堆栈 $st'); _loginFailApiCode = res.code;
} finally { _loginFailApiMsg = res.msg;
if (!completer.isCompleted) { _markStartupFailed(_friendlyHintForFailedLogin(res));
completer.complete();
isLoginComplete.value = true;
}
} }
} }

View File

@ -379,7 +379,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
if (refresh) _tasks = []; if (refresh) _tasks = [];
_loading = false; _loading = false;
_loadingMore = 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) { } catch (e) {
@ -388,7 +388,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
if (refresh) _tasks = []; if (refresh) _tasks = [];
_loading = false; _loading = false;
_loadingMore = false; _loadingMore = false;
_error = e.toString(); _error = 'Something went wrong while loading your gallery. Please try again.';
}); });
} }
} }

View File

@ -284,7 +284,7 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
'Generation failed: ${e.toString().replaceAll('Exception: ', '')}'), 'We could not start generation right now. Please try again.'),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );

View File

@ -63,7 +63,7 @@ class _GenerationResultScreenState extends State<GenerationResultScreen> {
if (mounted) { if (mounted) {
setState(() { setState(() {
_videoLoading = false; _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) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _errorText = e.toString().replaceAll('Exception: ', '')); setState(() => _errorText = 'Unable to submit your report right now. Please try again.');
} }
} finally { } finally {
if (mounted) { if (mounted) {

View File

@ -240,6 +240,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
/// ///
bool get _isListLoading { bool get _isListLoading {
if (!AuthService.isLoginComplete.value) return false; 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 (_showVideoMenu) {
if (_categoriesLoading) return true; if (_categoriesLoading) return true;
if (_selectedCategory?.id == kExtCategoryId) { if (_selectedCategory?.id == kExtCategoryId) {

View File

@ -422,11 +422,11 @@ class _DeleteAccountDialogState extends State<_DeleteAccountDialog> {
); );
} }
} else { } else {
setState(() => _errorText = res.msg.isNotEmpty ? res.msg : 'Delete failed'); setState(() => _errorText = 'Unable to delete your account right now. Please try again.');
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _errorText = e.toString().replaceAll('Exception: ', '')); setState(() => _errorText = 'Something went wrong while deleting your account. Please try again.');
} }
} finally { } finally {
if (mounted) { if (mounted) {

View File

@ -119,7 +119,7 @@ class _RechargeScreenState extends State<RechargeScreen>
setState(() { setState(() {
_activities = []; _activities = [];
_loadingTiers = false; _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) { } catch (e) {
@ -127,7 +127,7 @@ class _RechargeScreenState extends State<RechargeScreen>
setState(() { setState(() {
_activities = []; _activities = [];
_loadingTiers = false; _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) { if (!methodsRes.isSuccess || methodsRes.data == null) {
_showSnackBar( _showSnackBar(
context, context,
methodsRes.msg.isNotEmpty 'Unable to load payment methods right now. Please try again shortly.',
? methodsRes.msg
: 'Failed to load payment methods',
isError: true, isError: true,
); );
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
@ -239,7 +237,7 @@ class _RechargeScreenState extends State<RechargeScreen>
); );
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true);
} }
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
} finally { } finally {
@ -271,7 +269,7 @@ class _RechargeScreenState extends State<RechargeScreen>
if (!createRes.isSuccess) { if (!createRes.isSuccess) {
_showSnackBar( _showSnackBar(
context, context,
createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order', 'Unable to create your order right now. Please try again.',
isError: true, isError: true,
); );
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
@ -342,7 +340,7 @@ class _RechargeScreenState extends State<RechargeScreen>
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true);
} }
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
} finally { } finally {
@ -435,7 +433,7 @@ class _RechargeScreenState extends State<RechargeScreen>
if (!createRes.isSuccess) { if (!createRes.isSuccess) {
_showSnackBar( _showSnackBar(
context, context,
createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order', 'Unable to create your order right now. Please try again.',
isError: true, isError: true,
); );
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
@ -449,7 +447,7 @@ class _RechargeScreenState extends State<RechargeScreen>
serverOrderId: orderId, userId: userId); serverOrderId: orderId, userId: userId);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true);
} }
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();
} finally { } finally {
@ -527,7 +525,7 @@ class _RechargeScreenState extends State<RechargeScreen>
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_showSnackBar(context, 'Google Pay error: ${e.toString()}', _showSnackBar(context, 'Google Pay is temporarily unavailable. Please try again.',
isError: true); isError: true);
} }
AdjustEvents.trackPaymentFailed(); AdjustEvents.trackPaymentFailed();

View File

@ -18,7 +18,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await ensureDeviceMemoryProfileInitialized(); await ensureDeviceMemoryProfileInitialized();
_initAdjust(); _initAdjust();
// await ReferrerService.init()Adjust [AuthService.init] await getReferrer() // Do not await ReferrerService.init() hereit can block first frame (Adjust may take many seconds). AuthService.init awaits getReferrer().
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
statusBarColor: AppColors.surface, statusBarColor: AppColors.surface,
@ -34,8 +34,11 @@ void main() async {
GooglePlayPurchaseService.startPendingPurchaseListener(); GooglePlayPurchaseService.startPendingPurchaseListener();
} }
// completePurchase // completePurchase
AuthService.loginComplete AuthService.loginComplete.then((_) {
.then((_) => GooglePlayPurchaseService.runOrderRecovery()); if (AuthService.startupSucceeded.value) {
GooglePlayPurchaseService.runOrderRecovery();
}
});
} }
void _initAdjust() { void _initAdjust() {