Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
15 changes: 5 additions & 10 deletions mobile/lib/providers/notification_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,17 @@ class NotificationsNotifier extends AsyncNotifier<NotificationsState> {
int _currentPage = 0;
static const _pageSize = 20;
final _toggleReadInFlight = <int>{};
String? _lastUserId;

@override
Future<NotificationsState> 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<NotificationsState> _fetchInitialData(String userId) async {
Expand Down
78 changes: 43 additions & 35 deletions mobile/lib/screens/notifications_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -226,40 +226,43 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen>
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),
),
),
],
),
),
),
),
],
],
),
);
}

Expand Down Expand Up @@ -334,12 +337,15 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen>
);
}

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

Expand Down Expand Up @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion mobile/lib/services/dio_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> &&
responseData['type'] == 'security';
if (err.response?.statusCode == 403 &&
isSecurityType &&
appCheckError != null) {
AppLogger.e(
'DioService: 403 + App Check error detected. Triggering security lockdown.',
Exception(appCheckError),
Expand Down
7 changes: 3 additions & 4 deletions mobile/lib/services/security_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -436,7 +435,7 @@ class SecurityService {
: (backendReason ?? 'Security verification failed'),
type: AppExceptionType.unauthorized,
details: {
...?data,
...data,
'type': 'security',
'reason': finalReason,
'appCheckError': appCheckError,
Expand Down
2 changes: 1 addition & 1 deletion mobile/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion mobile/test/services/security_service_coverage_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ void main() {
response: Response(
requestOptions: options,
statusCode: 403,
data: {'error': 'Forbidden'},
data: {
'error': 'Forbidden',
'type': 'security',
},
),
type: DioExceptionType.badResponse,
);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghostclass",
"version": "4.4.8",
"version": "4.4.9",
"private": true,
"engines": {
"node": ">=22.12.0",
Expand Down
2 changes: 1 addition & 1 deletion public/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/app/(protected)/dashboard/DashboardClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading