diff --git a/.example.env b/.example.env index 30e71fc8..aa94d094 100644 --- a/.example.env +++ b/.example.env @@ -44,7 +44,7 @@ NEXT_PUBLIC_APP_NAME=GhostClass # âš ī¸ App version displayed in footer and health checks # 🔨 Build-time (Infisical `/build-time` folder) -NEXT_PUBLIC_APP_VERSION=4.4.1 +NEXT_PUBLIC_APP_VERSION=4.4.2 # âš ī¸ Your production domain WITHOUT https:// # All URL-based variables are derived from this. @@ -359,7 +359,7 @@ JWE_PRIVATE_KEY= # âš ī¸ Minimum supported app version required to bypass forced update # 🚀 Runtime (Infisical `/runtime` folder → Server Env Var) -MIN_APP_VERSION=4.4.1 +MIN_APP_VERSION=4.4.2 # â„šī¸ Enforce Firebase App Check for all mobile clients in production # Valid: "true", "false" (default: false in dev, true recommended in prod) diff --git a/mobile/lib/config/app_config.dart b/mobile/lib/config/app_config.dart index ccdb16cf..c860c918 100644 --- a/mobile/lib/config/app_config.dart +++ b/mobile/lib/config/app_config.dart @@ -75,7 +75,7 @@ class AppConfig { /// Current application version (derived from Infisical compilation injection). static String get appVersion => - const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.1'); + const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.2'); /// Commit SHA injected by CI for release builds. static String get appCommitSha => diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c143c802..7aa83dd7 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -142,12 +142,14 @@ void main() async { try { await _initializeFirebase(); - // Initialize Analytics after Firebase is ready - try { - await AnalyticsService.initialize(); - } on Object catch (_) { - AppLogger.e('Analytics initialization failed'); - } + // Initialize Analytics after Firebase is ready — do not block startup + AppLogger.safeUnawait( + AnalyticsService.initialize().catchError( + (Object e, StackTrace st) => + AppLogger.e('Analytics initialization failed', e, st), + ), + 'Analytics init', + ); AppLogger.i('đŸ›Ąī¸ [FIREBASE SHIELD] Initializing App Check...'); await SecurityInitializer.initialize(); @@ -175,10 +177,14 @@ void main() async { // Eagerly pre-warm cryptographic services concurrently while other SDKs/Fonts initialize AppLogger.safeUnawait(JweService.instance.preWarm(), 'JWE pre-warm'); - await GoogleFonts.pendingFonts([ - GoogleFonts.manrope(), - GoogleFonts.firaCode(), - ]); + // Defer font pre-warm so UI can render faster + AppLogger.safeUnawait( + GoogleFonts.pendingFonts([ + GoogleFonts.manrope(), + GoogleFonts.firaCode(), + ]), + 'Fonts pre-warm', + ); runApp( ProviderScope( diff --git a/mobile/lib/screens/splash_screen.dart b/mobile/lib/screens/splash_screen.dart index 4af8fc3d..9aea123e 100644 --- a/mobile/lib/screens/splash_screen.dart +++ b/mobile/lib/screens/splash_screen.dart @@ -63,21 +63,33 @@ class _SplashScreenState extends ConsumerState { Future _initializeApp() async { // 1. Proactively pre-warm security layers while logo is showing - final jwePreWarm = JweService.instance.preWarm(); - final apiPreWarm = ref.read(apiServiceProvider).preWarm(); + // Keep the splash visible for 2s to improve perceived startup time final splashHold = Future.delayed( - const Duration(milliseconds: 3000), + const Duration(milliseconds: 2000), () { - AppLogger.i('SplashScreen: 3s delay completed'); + AppLogger.i('SplashScreen: 2s delay completed'); }, ); - try { - await jwePreWarm; - await apiPreWarm; - } on Object catch (e) { - AppLogger.e('SplashScreen: JWKS/API pre-warm failed', e); - } + // Kick off non-critical pre-warms in the background so they do not block + AppLogger.safeUnawait( + JweService.instance.preWarm().catchError( + (Object e, StackTrace st) => + AppLogger.e('SplashScreen: JWE pre-warm failed', e, st), + ), + 'SplashScreen: JWE pre-warm', + ); + + AppLogger.safeUnawait( + ref + .read(apiServiceProvider) + .preWarm() + .catchError( + (Object e, StackTrace st) => + AppLogger.e('SplashScreen: API pre-warm failed', e, st), + ), + 'SplashScreen: API pre-warm', + ); // 2. Critical Security Check First try { diff --git a/mobile/lib/services/dio_service.dart b/mobile/lib/services/dio_service.dart index c1a1f910..7befdb4d 100644 --- a/mobile/lib/services/dio_service.dart +++ b/mobile/lib/services/dio_service.dart @@ -109,6 +109,8 @@ class DioService { // Futures to deduplicate parallel token requests Future? _tokenFetchInFlight; Future? _limitedTokenFetchInFlight; + // Instrumentation: count how many times we requested a limited-use token + static int _limitedTokenRequestCount = 0; void _handle401(RequestOptions options) { if (suppress401) return; @@ -133,6 +135,10 @@ class DioService { if (useLimited) { var isNew = false; if (_limitedTokenFetchInFlight == null) { + _limitedTokenRequestCount++; + AppLogger.d( + 'DioService: getLimitedUseToken requested (count: $_limitedTokenRequestCount)', + ); _limitedTokenFetchInFlight = _appCheck.getLimitedUseToken(); isNew = true; } @@ -141,7 +147,7 @@ class DioService { ); if (isNew) { AppLogger.safeUnawait( - Future.delayed(const Duration(seconds: 5), () { + Future.delayed(const Duration(seconds: 30), () { _limitedTokenFetchInFlight = null; }).catchError( (Object e, StackTrace st) { diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fd1bb9d5..0e9f9312 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 4.4.1+1 +version: 4.4.2+1 environment: sdk: ^3.11.4 diff --git a/package-lock.json b/package-lock.json index acdb101f..ebf4fb76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostclass", - "version": "4.4.1", + "version": "4.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostclass", - "version": "4.4.1", + "version": "4.4.2", "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/package.json b/package.json index d72a99d8..5db1c868 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostclass", - "version": "4.4.1", + "version": "4.4.2", "private": true, "engines": { "node": ">=22.12.0", diff --git a/public/openapi/openapi.yaml b/public/openapi/openapi.yaml index a6b718c8..1d58824d 100644 --- a/public/openapi/openapi.yaml +++ b/public/openapi/openapi.yaml @@ -6,7 +6,7 @@ openapi: 3.1.0 info: title: GhostClass API - version: 4.4.1 + version: 4.4.2 description: | **GhostClass API** provides endpoints for authentication, profile synchronization, attendance integrations with EzyGo, telemetry, and build provenance.