From f5b1328011743bf7b1fc61ca4b182a2ccf362640 Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Mon, 1 Jun 2026 12:38:32 +0000 Subject: [PATCH 1/3] feat: implement authentication request retries, timeout configurations, and enhanced error handling for network timeouts --- .example.env | 4 +- mobile/lib/config/app_config.dart | 2 +- mobile/lib/logic/error_utils.dart | 7 ++++ mobile/lib/services/auth_service.dart | 60 ++++++++++++++++++++++++++- mobile/pubspec.yaml | 2 +- package-lock.json | 4 +- package.json | 2 +- public/openapi/openapi.yaml | 2 +- 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/.example.env b/.example.env index c13e7fe6..8409d2db 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.6 +NEXT_PUBLIC_APP_VERSION=4.4.7 # âš ī¸ Your production domain WITHOUT https:// # All URL-based variables are derived from this. @@ -365,7 +365,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.6 +MIN_APP_VERSION=4.4.7 # â„šī¸ 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 10c700d4..89b1475a 100644 --- a/mobile/lib/config/app_config.dart +++ b/mobile/lib/config/app_config.dart @@ -76,7 +76,7 @@ class AppConfig { /// Current application version (derived from Infisical compilation injection). static String get appVersion => - const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.6'); + const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.7'); /// Commit SHA injected by CI for release builds. static String get appCommitSha => diff --git a/mobile/lib/logic/error_utils.dart b/mobile/lib/logic/error_utils.dart index 019fccd2..5c20cda8 100644 --- a/mobile/lib/logic/error_utils.dart +++ b/mobile/lib/logic/error_utils.dart @@ -33,6 +33,13 @@ String formatApiError(dynamic response, String context) { } else if (response is DioException) { status = response.response?.statusCode; final data = response.response?.data; + + if (response.type == DioExceptionType.connectionTimeout || + response.type == DioExceptionType.sendTimeout || + response.type == DioExceptionType.receiveTimeout) { + return 'The server is taking longer than expected. Please try again in a moment.'; + } + if (data is Map) { code = (data['code'] ?? '').toString(); message = (data['message'] ?? data['error'] ?? data['detail'] ?? '') diff --git a/mobile/lib/services/auth_service.dart b/mobile/lib/services/auth_service.dart index 95ce4429..3881b57e 100644 --- a/mobile/lib/services/auth_service.dart +++ b/mobile/lib/services/auth_service.dart @@ -1,8 +1,10 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ghostclass/config/app_config.dart'; import 'package:ghostclass/logic/app_exception.dart'; import 'package:ghostclass/services/dio_service.dart'; +import 'package:ghostclass/services/logger.dart'; import 'package:ghostclass/services/secure_storage.dart'; /// AuthService @@ -14,6 +16,10 @@ class AuthService { final Ref _ref; static final String _ghostclassBaseUrl = AppConfig.ghostclassApiUrl; static final String _ezygoAuthUrl = AppConfig.ezygoAuthUrl; + static const Duration _loginTimeout = kDebugMode + ? Duration(seconds: 45) + : Duration(seconds: 30); + static const Duration _provisionRetryBackoff = Duration(milliseconds: 500); Dio get _dio => _ref.read(dioServiceProvider).dio; @@ -21,7 +27,13 @@ class AuthService { required String username, required String password, }) async { + final flowWatch = Stopwatch()..start(); + + AppLogger.d('AuthService: loginAndProvision start'); final ezygoResponse = await loginEzygo(username, password); + AppLogger.d( + 'AuthService: loginEzygo completed in ${flowWatch.elapsedMilliseconds}ms (status: ${ezygoResponse.statusCode})', + ); if (ezygoResponse.statusCode != 200) return ezygoResponse; final data = ezygoResponse.data as Map?; @@ -34,7 +46,12 @@ class AuthService { ); } - final bridgeResponse = await provisionGhostClassSession(ezygoToken); + final bridgeResponse = await _provisionGhostClassSessionWithRetry( + ezygoToken, + ); + AppLogger.d( + 'AuthService: provisionGhostClassSession completed in ${flowWatch.elapsedMilliseconds}ms (status: ${bridgeResponse.statusCode})', + ); final bridgeData = bridgeResponse.data; if (bridgeData is Map) { bridgeResponse.data = { @@ -46,6 +63,35 @@ class AuthService { return bridgeResponse; } + bool _isTransientProvisionFailure(Object error) { + if (error is DioException) { + return error.type == DioExceptionType.connectionTimeout || + error.type == DioExceptionType.receiveTimeout || + error.type == DioExceptionType.sendTimeout || + error.type == DioExceptionType.connectionError || + error.type == DioExceptionType.unknown; + } + return false; + } + + Future> _provisionGhostClassSessionWithRetry( + String ezygoToken, + ) async { + try { + return await provisionGhostClassSession(ezygoToken); + } on Object catch (e, st) { + if (!_isTransientProvisionFailure(e)) rethrow; + + AppLogger.e( + 'AuthService: provisionGhostClassSession transient failure. Retrying once...', + e, + st, + ); + await Future.delayed(_provisionRetryBackoff); + return provisionGhostClassSession(ezygoToken); + } + } + Future> loginEzygo(String username, String password) async { return _dio.post( _ezygoAuthUrl, @@ -54,7 +100,13 @@ class AuthService { 'password': password, 'stay_logged_in': true, }, - options: Options(validateStatus: (s) => s != null && s < 600), + options: Options( + connectTimeout: _loginTimeout, + receiveTimeout: _loginTimeout, + sendTimeout: _loginTimeout, + persistentConnection: false, + validateStatus: (s) => s != null && s < 600, + ), ); } @@ -65,6 +117,10 @@ class AuthService { '$_ghostclassBaseUrl/auth/save-token', data: {'token': ezygoToken.trim()}, options: Options( + connectTimeout: _loginTimeout, + receiveTimeout: _loginTimeout, + sendTimeout: _loginTimeout, + persistentConnection: false, extra: {'useLimitedToken': true}, validateStatus: (s) => s != null && s < 600, ), diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3cb31268..79f7469d 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.6+1 +version: 4.4.7+1 environment: sdk: ^3.11.4 diff --git a/package-lock.json b/package-lock.json index f60ac61a..68494108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostclass", - "version": "4.4.6", + "version": "4.4.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostclass", - "version": "4.4.6", + "version": "4.4.7", "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/package.json b/package.json index b55cc541..6d61355f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostclass", - "version": "4.4.6", + "version": "4.4.7", "private": true, "engines": { "node": ">=22.12.0", diff --git a/public/openapi/openapi.yaml b/public/openapi/openapi.yaml index 8303b738..43504d4a 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.6 + version: 4.4.7 description: | **GhostClass API** provides endpoints for authentication, profile synchronization, attendance integrations with EzyGo, telemetry, and build provenance. From 4b884ceaa1a403cb3c32ea3643014f45a48d6585 Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Mon, 1 Jun 2026 18:18:43 +0530 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Devanarayanan --- mobile/lib/services/auth_service.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/auth_service.dart b/mobile/lib/services/auth_service.dart index 3881b57e..4118e631 100644 --- a/mobile/lib/services/auth_service.dart +++ b/mobile/lib/services/auth_service.dart @@ -46,11 +46,14 @@ class AuthService { ); } + final provisionStartMs = flowWatch.elapsedMilliseconds; final bridgeResponse = await _provisionGhostClassSessionWithRetry( ezygoToken, ); + final provisionDurationMs = + flowWatch.elapsedMilliseconds - provisionStartMs; AppLogger.d( - 'AuthService: provisionGhostClassSession completed in ${flowWatch.elapsedMilliseconds}ms (status: ${bridgeResponse.statusCode})', + 'AuthService: provisionGhostClassSession completed in ${provisionDurationMs}ms (status: ${bridgeResponse.statusCode})', ); final bridgeData = bridgeResponse.data; if (bridgeData is Map) { From 2d5506623fdcc03dd492675918516fe054d48e04 Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Mon, 1 Jun 2026 18:18:58 +0530 Subject: [PATCH 3/3] Potential fix for timestamp Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Devanarayanan --- mobile/lib/services/auth_service.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/auth_service.dart b/mobile/lib/services/auth_service.dart index 4118e631..bb23bfd0 100644 --- a/mobile/lib/services/auth_service.dart +++ b/mobile/lib/services/auth_service.dart @@ -30,9 +30,11 @@ class AuthService { final flowWatch = Stopwatch()..start(); AppLogger.d('AuthService: loginAndProvision start'); + final loginStartMs = flowWatch.elapsedMilliseconds; final ezygoResponse = await loginEzygo(username, password); + final loginDurationMs = flowWatch.elapsedMilliseconds - loginStartMs; AppLogger.d( - 'AuthService: loginEzygo completed in ${flowWatch.elapsedMilliseconds}ms (status: ${ezygoResponse.statusCode})', + 'AuthService: loginEzygo completed in ${loginDurationMs}ms (status: ${ezygoResponse.statusCode})', ); if (ezygoResponse.statusCode != 200) return ezygoResponse;