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.5.3
NEXT_PUBLIC_APP_VERSION=4.5.4

# ⚠️ Your production domain WITHOUT https://
# All URL-based variables are derived from this.
Expand Down Expand Up @@ -371,7 +371,7 @@ REQUEST_SIGNATURE_MAX_AGE=600

# ⚠️ Minimum supported app version required to bypass forced update
# 🚀 Runtime (Infisical `/runtime` folder → Server Env Var)
MIN_APP_VERSION=4.5.3
MIN_APP_VERSION=4.5.4

# ℹ️ Enforce Firebase App Check for all mobile clients in production
# Valid: "true", "false" (default: false in dev, true recommended in prod)
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/build-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@ jobs:
run: |
python3 -m pip install check-jsonschema
check-jsonschema --schemafile https://json.schemastore.org/dependabot-2.0.json .github/dependabot.yml

10 changes: 3 additions & 7 deletions mobile/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# ProGuard rules for Flutter / native wrapper optimization
# Standard Android optimizations and preservation rules.

# Preserve Flutter wrapper classes if needed
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class plugins.flutter.io.** { *; }
# Preserve generated plugin registrant and Flutter plugin reflection entry points
-keep class plugins.flutter.io.** { *; }

# Suppress warnings for optional Play Core split install classes referenced by Flutter embedding
-dontwarn com.google.android.play.core.**

2 changes: 1 addition & 1 deletion mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:debuggable="false"
android:enableOnBackInvokedCallback="false"
android:enableOnBackInvokedCallback="true"
tools:ignore="HardcodedDebugMode">
<activity
android:name=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package com.devakesu.apps.ghostclass

import android.os.Bundle
import android.os.Debug
import android.view.MotionEvent
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity
import androidx.activity.enableEdgeToEdge
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.view.MotionEvent

class MainActivity : FlutterActivity() {
class MainActivity : FlutterFragmentActivity() {
private val CHANNEL = "com.devakesu.apps.ghostclass/security"
private var isCurrentWindowObscured = false

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
isCurrentWindowObscured = false
Expand Down Expand Up @@ -56,4 +63,3 @@ class MainActivity : FlutterActivity() {
}
}
}

2 changes: 1 addition & 1 deletion mobile/lib/config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class AppConfig {

/// Current application version (derived from Infisical compilation injection).
static String get appVersion =>
const String.fromEnvironment('APP_VERSION', defaultValue: '4.5.3');
const String.fromEnvironment('APP_VERSION', defaultValue: '4.5.4');

/// Commit SHA injected by CI for release builds.
static String get appCommitSha =>
Expand Down
3 changes: 2 additions & 1 deletion mobile/lib/screens/navigation_shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -534,7 +535,7 @@ class _NavigationShellState extends ConsumerState<NavigationShell> {
).colorScheme.surfaceContainer,
backgroundImage:
user?.profile?.avatarUrl != null
? NetworkImage(
? CachedNetworkImageProvider(
user!.profile!.avatarUrl!,
headers: {
'Origin':
Expand Down
15 changes: 10 additions & 5 deletions mobile/lib/screens/splash_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -220,10 +221,14 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
}

prewarm(dashboardFuture, 'dashboard');
prewarm(trackingFuture, 'tracking');
prewarm(leaveFuture, 'leave');
prewarm(scoreFuture, 'scores');
prewarm(notificationsFuture, 'notifications');
WidgetsBinding.instance.addPostFrameCallback((_) {
prewarm(trackingFuture, 'tracking');
prewarm(leaveFuture, 'leave');
Future.delayed(const Duration(milliseconds: 150), () {
prewarm(scoreFuture, 'scores');
prewarm(notificationsFuture, 'notifications');
});
});
}

@override
Expand Down Expand Up @@ -315,7 +320,7 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
finalUser.profile?.avatarUrl != null) {
try {
final _ = precacheImage(
NetworkImage(
CachedNetworkImageProvider(
finalUser.profile!.avatarUrl!,
headers: {
'Origin': AppConfig.supabaseOrigin,
Expand Down
62 changes: 48 additions & 14 deletions mobile/lib/services/api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,54 @@ class ApiService {

_syncInFlight = () async {
try {
final response = await client.get<dynamic>(
'${AppConfig.ghostclassApiUrl}/cron/sync?t=${now.millisecondsSinceEpoch}',
options: Options(
headers: {'Authorization': 'Bearer $supabaseToken'},
sendTimeout: kDebugMode
? const Duration(seconds: 45)
: const Duration(seconds: 30),
receiveTimeout: kDebugMode
? const Duration(seconds: 45)
: const Duration(seconds: 30),
),
);
_lastSyncAt = DateTime.now();
return response;
final currentToken =
(supabaseToken.isNotEmpty
? supabaseToken
: Supabase.instance.client.auth.currentSession?.accessToken) ??
'';

Future<Response<dynamic>> makeSyncCall(String token) {
return client.get<dynamic>(
'${AppConfig.ghostclassApiUrl}/cron/sync?t=${now.millisecondsSinceEpoch}',
options: Options(
headers: {'Authorization': 'Bearer $token'},
sendTimeout: kDebugMode
? const Duration(seconds: 45)
: const Duration(seconds: 30),
receiveTimeout: kDebugMode
? const Duration(seconds: 45)
: const Duration(seconds: 30),
),
);
}

try {
final response = await makeSyncCall(currentToken);
_lastSyncAt = DateTime.now();
return response;
} on DioException catch (dioErr) {
if (dioErr.response?.statusCode == 401) {
AppLogger.i(
'ApiService: Sync returned 401, attempting token refresh...',
);
try {
final refreshed = await Supabase.instance.client.auth
.refreshSession();
final freshToken = refreshed.session?.accessToken;
if (freshToken != null && freshToken.isNotEmpty) {
final response = await makeSyncCall(freshToken);
_lastSyncAt = DateTime.now();
return response;
}
} on Object catch (refreshErr) {
AppLogger.e(
'ApiService: Session refresh during 401 sync retry failed',
refreshErr,
);
}
}
rethrow;
}
} on Object catch (e) {
AppLogger.e('ApiService: Background sync failed', e);
rethrow;
Expand Down
7 changes: 7 additions & 0 deletions mobile/lib/services/push_notification_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PushNotificationService {
Timer? _deferredAuthTimer;
bool _initialized = false;
bool get isInitialized => _initialized;
bool _isRegistering = false;

Dio get _dio => _ref.read(dioServiceProvider).dio;
SecureStorageService get _storage => _ref.read(secureStorageProvider);
Expand Down Expand Up @@ -273,6 +274,8 @@ class PushNotificationService {

/// Synchronises the secure push token with the backend storage route.
Future<void> _syncTokenWithBackend(String token, {bool force = false}) async {
if (_isRegistering) return;
_isRegistering = true;
try {
if (!_ref.mounted) return;
final cachedToken = await _storage.getNormalizedFcmToken();
Expand All @@ -298,6 +301,8 @@ class PushNotificationService {
final session = data.session;
if (session != null) {
try {
final latestCached = await _storage.getNormalizedFcmToken();
if (latestCached == token) return;
final accessToken = session.accessToken;
final response = await _registerTokenHttp(token, accessToken);
if (response.statusCode == 200) {
Expand Down Expand Up @@ -421,6 +426,8 @@ class PushNotificationService {
'PushNotificationService: analytics fcm_registered (exception)',
);
} on Object catch (_) {}
} finally {
_isRegistering = false;
}
}

Expand Down
3 changes: 2 additions & 1 deletion mobile/lib/widgets/profile/profile_widgets.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:ghostclass/config/app_config.dart';
import 'package:google_fonts/google_fonts.dart';
Expand Down Expand Up @@ -48,7 +49,7 @@ class ProfileHeader extends StatelessWidget {
),
image: avatarUrl != null
? DecorationImage(
image: NetworkImage(
image: CachedNetworkImageProvider(
avatarUrl!,
headers: {
'Origin': AppConfig.supabaseOrigin,
Expand Down
96 changes: 96 additions & 0 deletions mobile/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.7"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
characters:
dependency: transitive
description:
Expand Down Expand Up @@ -534,6 +558,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "1de7849213b4c73c85aca7e0ac687a9a5d82ccdb594366b9dcc26cb6a2189cd2"
url: "https://pub.dev"
source: hosted
version: "3.4.2"
flutter_launcher_icons:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -992,6 +1024,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.5.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_config:
dependency: transitive
description:
Expand Down Expand Up @@ -1264,6 +1304,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.4"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
sentry:
dependency: transitive
description:
Expand Down Expand Up @@ -1429,6 +1477,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
url: "https://pub.dev"
source: hosted
version: "2.4.3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
url: "https://pub.dev"
source: hosted
version: "2.5.11"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
url: "https://pub.dev"
source: hosted
version: "2.4.3+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
url: "https://pub.dev"
source: hosted
version: "2.4.1"
stack_trace:
dependency: transitive
description:
Expand Down Expand Up @@ -1501,6 +1589,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.17.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423"
url: "https://pub.dev"
source: hosted
version: "3.4.1+1"
system_info2:
dependency: transitive
description:
Expand Down
Loading
Loading