From 56993d07fc88773735b7b0dfa99a4c87170b776a Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Mon, 1 Jun 2026 16:27:47 +0000 Subject: [PATCH] feat: update notification UI padding, refine security error handling, and optimize dashboard sync and state management --- .example.env | 4 +- mobile/lib/config/app_config.dart | 2 +- .../lib/providers/notification_provider.dart | 15 ++-- mobile/lib/screens/notifications_screen.dart | 78 ++++++++++--------- mobile/lib/services/dio_service.dart | 8 +- mobile/lib/services/security_service.dart | 7 +- mobile/pubspec.yaml | 2 +- .../security_service_coverage_test.dart | 5 +- package-lock.json | 4 +- package.json | 2 +- public/openapi/openapi.yaml | 2 +- .../(protected)/dashboard/DashboardClient.tsx | 2 +- 12 files changed, 71 insertions(+), 60 deletions(-) diff --git a/.example.env b/.example.env index a55a066d..ee216b1a 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.8 +NEXT_PUBLIC_APP_VERSION=4.4.9 # âš ī¸ 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.8 +MIN_APP_VERSION=4.4.9 # â„šī¸ 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 1f7b5b39..e90aa4d6 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.8'); + const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.9'); /// Commit SHA injected by CI for release builds. static String get appCommitSha => diff --git a/mobile/lib/providers/notification_provider.dart b/mobile/lib/providers/notification_provider.dart index 26d52182..23548305 100644 --- a/mobile/lib/providers/notification_provider.dart +++ b/mobile/lib/providers/notification_provider.dart @@ -82,22 +82,17 @@ class NotificationsNotifier extends AsyncNotifier { int _currentPage = 0; static const _pageSize = 20; final _toggleReadInFlight = {}; - String? _lastUserId; @override Future build() async { - final user = ref.watch(authProvider).value; - if (user == null) { - _lastUserId = null; + final userId = ref.watch( + authProvider.select((v) => v.value?.supabaseUserId), + ); + if (userId == null) { return NotificationsState.empty(); } - if (_lastUserId == user.supabaseUserId && state.hasValue) { - return state.value!; - } - - _lastUserId = user.supabaseUserId; - return _fetchInitialData(user.supabaseUserId); + return _fetchInitialData(userId); } Future _fetchInitialData(String userId) async { diff --git a/mobile/lib/screens/notifications_screen.dart b/mobile/lib/screens/notifications_screen.dart index f2a1a744..018de743 100644 --- a/mobile/lib/screens/notifications_screen.dart +++ b/mobile/lib/screens/notifications_screen.dart @@ -226,40 +226,43 @@ class _NotificationsScreenState extends ConsumerState NotificationsState data, ) { if (data.allNotifications.isEmpty) { - return CustomScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - LucideIcons.bellOff, - size: 64, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.2), - ), - const SizedBox(height: 16), - const Text('All caught up!'), - Text( - 'You have no new notifications.', - style: TextStyle( + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + LucideIcons.bellOff, + size: 64, color: Theme.of( context, - ).colorScheme.onSurface.withValues(alpha: 0.5), + ).colorScheme.onSurface.withValues(alpha: 0.2), ), - ), - ], + const SizedBox(height: 16), + const Text('All caught up!'), + Text( + 'You have no new notifications.', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), ), ), - ), - ], + ], + ), ); } @@ -334,12 +337,15 @@ class _NotificationsScreenState extends ConsumerState ); } - return CustomScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: slivers, ), - slivers: slivers, ); } @@ -530,7 +536,9 @@ class _NotificationCard extends ConsumerWidget { color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, ), - ), + ) + else + const SizedBox(width: 18), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( diff --git a/mobile/lib/services/dio_service.dart b/mobile/lib/services/dio_service.dart index c1bcb672..91bc9ef5 100644 --- a/mobile/lib/services/dio_service.dart +++ b/mobile/lib/services/dio_service.dart @@ -65,7 +65,13 @@ class DioService { // lockdown event so security flows can react (e.g. forced logout). final appCheckError = err.requestOptions.extra['appCheckError'] as String?; - if (err.response?.statusCode == 403 && appCheckError != null) { + final responseData = err.response?.data; + final isSecurityType = + responseData is Map && + responseData['type'] == 'security'; + if (err.response?.statusCode == 403 && + isSecurityType && + appCheckError != null) { AppLogger.e( 'DioService: 403 + App Check error detected. Triggering security lockdown.', Exception(appCheckError), diff --git a/mobile/lib/services/security_service.dart b/mobile/lib/services/security_service.dart index d21fdfbd..130ee835 100644 --- a/mobile/lib/services/security_service.dart +++ b/mobile/lib/services/security_service.dart @@ -416,10 +416,9 @@ class SecurityService { (data?['error'] as String?) ?? (data?['appCheckError'] as String?); - if (isSecurityType || - (e.response?.statusCode == 403 && appCheckError != null)) { + if (isSecurityType) { final action = - data?['action'] ?? + data['action'] ?? 'Please ensure your device is not rooted, you are using the official app, and you have a stable internet connection.'; final finalReason = @@ -436,7 +435,7 @@ class SecurityService { : (backendReason ?? 'Security verification failed'), type: AppExceptionType.unauthorized, details: { - ...?data, + ...data, 'type': 'security', 'reason': finalReason, 'appCheckError': appCheckError, diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e4aa3fa1..57e183a0 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.8+1 +version: 4.4.9+1 environment: sdk: ^3.11.4 diff --git a/mobile/test/services/security_service_coverage_test.dart b/mobile/test/services/security_service_coverage_test.dart index ebc117a7..e338bada 100644 --- a/mobile/test/services/security_service_coverage_test.dart +++ b/mobile/test/services/security_service_coverage_test.dart @@ -108,7 +108,10 @@ void main() { response: Response( requestOptions: options, statusCode: 403, - data: {'error': 'Forbidden'}, + data: { + 'error': 'Forbidden', + 'type': 'security', + }, ), type: DioExceptionType.badResponse, ); diff --git a/package-lock.json b/package-lock.json index 1d4ef492..22fb013a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostclass", - "version": "4.4.8", + "version": "4.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostclass", - "version": "4.4.8", + "version": "4.4.9", "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/package.json b/package.json index 86002c51..06b643ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostclass", - "version": "4.4.8", + "version": "4.4.9", "private": true, "engines": { "node": ">=22.12.0", diff --git a/public/openapi/openapi.yaml b/public/openapi/openapi.yaml index 4645d3f3..737107f2 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.8 + version: 4.4.9 description: | **GhostClass API** provides endpoints for authentication, profile synchronization, attendance integrations with EzyGo, telemetry, and build provenance. diff --git a/src/app/(protected)/dashboard/DashboardClient.tsx b/src/app/(protected)/dashboard/DashboardClient.tsx index 3a50536b..eef6fd6d 100644 --- a/src/app/(protected)/dashboard/DashboardClient.tsx +++ b/src/app/(protected)/dashboard/DashboardClient.tsx @@ -487,7 +487,7 @@ export default function DashboardClient({ initialData, serverError }: DashboardC sentryLocation: "DashboardClient", sentryTag: "background_sync", onSuccess: async (data) => { - const changed = (data.deletions ?? 0) + (data.updates ?? 0) + (data.conflicts ?? 0); + const changed = (data.deletions ?? 0) + (data.updates ?? 0); if (changed > 0) { toast.info("Dashboard Updated", { description: "Background sync applied latest attendance changes.",