优化:添加登录失败提示

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() {
super.initState();
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((_) {
// +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(
'请检查 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,
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 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,
children: [
HomeScreen(isActive: widget.currentTab == NavTab.home),
GalleryScreen(isActive: widget.currentTab == NavTab.gallery),
ProfileScreen(isActive: widget.currentTab == NavTab.profile),
],
),
),
],
),
bottomNavigationBar: BottomNavBar(

View File

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

View File

@ -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);
}

View File

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

View File

@ -49,9 +49,42 @@ class AuthService {
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);
/// `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
static Future<void> get loginComplete => _loginFuture ?? Future<void>.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 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 {
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 Future<void>.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<void>.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<String, dynamic>?;
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 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));
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<void>.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 couldnt 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<String, dynamic>?;
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));
}
}

View File

@ -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.';
});
}
}

View File

@ -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,
),
);

View File

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

View File

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

View File

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

View File

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

View File

@ -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() hereit 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() {