1
0
forked from firka/firka

2 Commits

Author SHA1 Message Date
checkedear
7242437231 Merge branch 'dev' of https://git.firka.app/firka/firka into notifications 2026-06-23 12:13:09 +02:00
checkedear
50f0332842 feat: notifications 2026-06-23 12:13:02 +02:00
9 changed files with 232 additions and 19 deletions

View File

@@ -12,6 +12,7 @@ android {
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -71,6 +72,7 @@ configurations.all {
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("androidx.glance:glance-appwidget:1.1.1")
implementation("com.google.android.gms:play-services-wearable:18.1.0")
}
@@ -90,4 +92,4 @@ tasks.matching { it.name.startsWith("compileFlutterBuild") }.configureEach {
}
flutter {
source = "../.."
}
}

View File

@@ -3,7 +3,9 @@ import 'dart:convert';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firka/core/extensions.dart';
import 'package:firka/core/settings.dart';
import 'package:firka/data/models/generic_cache_model.dart';
import 'package:firka/data/models/timetable_cache_model.dart';
import 'package:isar_community/isar.dart';
@@ -39,6 +41,65 @@ class KretaClient {
bool get needsReauth => _reauthCubit.state.needsReauth;
Future<Response<dynamic>> sendNotifReq(
String method,
TokenModel model,
String? fcmToken, {
bool auth = true,
}) async {
var isGuardian = model.studentId!.contains("G0");
return await dio.request(
"https://kretaglobalmobileapi2.ekreta.hu/api/v3/Registration",
options: Options(
method: method,
headers: {
"accept": "*/*",
"user-agent": Constants.userAgent,
if (auth) "Authorization": "Bearer ${model.accessToken}",
"apiKey": "7856d350-1fda-45f5-822d-e1a2f3f1acf0",
},
),
queryParameters: <String, String>{
"RegistrationId": ?model.registrationId,
"Handle": ?fcmToken,
"NotificationRole": isGuardian ? "2" : "1",
"NotificationEnvironment": isGuardian
? "Gondviselo_Native"
: "Tanulo_Native",
"NotificationType": "1",
if (method == "POST") "Platform": "fcmv1",
"NotificationSource": "Kreta",
},
);
}
Future<void> updateFCMToken({String? fcmToken}) async {
fcmToken = fcmToken ?? (await FirebaseMessaging.instance.getToken())!;
Response<dynamic> resp;
if (model.registrationId == null) {
resp = await sendNotifReq("POST", model, fcmToken, auth: true);
if (resp.statusCode == 200) {
model.registrationId = resp.data["registrationId"];
model.fcmToken = fcmToken;
logger.info("New registr: ${model.registrationId}");
} else {
logger.info("POST RESP");
logger.info(resp.statusCode);
logger.info(resp);
}
} else if (fcmToken != model.fcmToken) {
logger.info("FCM mismatch!");
model.registrationId = null;
await updateFCMToken(fcmToken: fcmToken);
} else {
logger.info("FCM already registered!");
}
}
void clearReauthFlag() {
_reauthCubit.clear();
debugPrint('[KretaClient] Reauth flag cleared');
@@ -83,7 +144,9 @@ class KretaClient {
}
final extended = await extendToken(sourceToken);
return TokenModel.fromResp(extended);
return TokenModel.fromResp(extended)
..registrationId ??= sourceToken.registrationId
..fcmToken ??= sourceToken.fcmToken;
} finally {
if (Platform.isIOS && studentIdNorm != null && leaseOperationId != null) {
await WatchSyncHelper.releaseIPhoneRefreshLease(
@@ -169,11 +232,13 @@ class KretaClient {
try {
var tokenModel = await _refreshModelWithCrossDeviceLease(model);
model = tokenModel;
await updateFCMToken();
await isar.writeTxn(() async {
await isar.tokenModels.put(tokenModel);
});
model = tokenModel;
await _syncTokenToAppleTargets(model);
clearReauthFlag();
logger.info("[Recovery] Step 1 SUCCESS: Local refresh succeeded");
@@ -304,7 +369,7 @@ class KretaClient {
return true;
}
Future<T> _mutexCallback<T>(Future<T> Function() callback) async {
Future<T> mutexCallback<T>(Future<T> Function() callback) async {
const maxWaitTime = Duration(seconds: 30);
if (_tokenMutexCompleter != null) {
@@ -337,7 +402,7 @@ class KretaClient {
}
Future<Response> _authReq(String method, String url, [Object? data]) async {
var localToken = await _mutexCallback<String>(() async {
var localToken = await mutexCallback<String>(() async {
var now = timeNow();
if (now.millisecondsSinceEpoch >=
@@ -366,7 +431,11 @@ class KretaClient {
return await dio.get(
url,
options: Options(method: method, headers: headers),
options: Options(
method: method,
headers: headers,
receiveTimeout: Duration(seconds: 20),
),
data: data,
);
}
@@ -381,7 +450,7 @@ class KretaClient {
try {
logger.finest("Sending authenticated request to: $url");
resp = await _authReq(method, url, data);
if (!url.endsWith("TanuloAdatlap")) {
if (!isDebug() && !url.endsWith("TanuloAdatlap")) {
logger.finest("Response: ${resp.statusCode} ${resp.data}");
}
@@ -404,6 +473,9 @@ class KretaClient {
);
} else {
logger.shout("Request to url: $url failed", ex.toString());
if (ex is Exception) {
logger.shout(ex);
}
}
rethrow;
@@ -733,8 +805,10 @@ class KretaClient {
}) async {
if (from == null && to == null) {
DateTime now = timeNow();
/* you are taking too long °w°
DateTime start = now.copyWith(month: 9, day: 1);
from = now.isBefore(start) ? start.subtract(Duration(days: 365)) : start;
from = now.isBefore(start) ? start.subtract(Duration(days: 365)) : start;*/
from = now.subtract(Duration(days: 14));
}
return await _genericListedCachingGet(
CacheId.getHomework,

View File

@@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:go_router/go_router.dart';
import 'package:firka/api/client/kreta_client.dart';
import 'package:firka/core/bloc/home_refresh_cubit.dart';
@@ -17,6 +18,7 @@ import 'package:isar_community/isar.dart';
import 'dart:io';
late final Logger logger;
late final FlutterLocalNotificationsPlugin flnp;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late AppInitialization initData;

View File

@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firka/api/consts.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@@ -25,7 +28,9 @@ import 'package:firka/l10n/app_localizations_en.dart';
import 'package:firka/l10n/app_localizations_hu.dart';
import 'package:firka/core/swear_generator.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:intl/intl.dart';
import 'package:kreta_api/kreta_api.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:isar_community/isar.dart';
@@ -299,6 +304,49 @@ Future<AppInitialization> initializeApp() async {
await _initData(init);
initData = init;
initDone = true;
flnp = FlutterLocalNotificationsPlugin();
const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings(
requestSoundPermission: true,
requestBadgePermission: true,
requestAlertPermission: false,
);
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('ic_notification');
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
);
await flnp.initialize(settings: initializationSettings);
Future<void> updateToken([String? fcm]) async {
TokenModel? token = pickActiveToken(
tokens: initData.tokens,
settings: initData.settings,
);
if (token == null) return;
try {
await initData.client.mutexCallback(() async {
if (!await initData.client.refreshTokenProactively()) {
throw TokenExpiredException();
}
});
} catch (e) {
return;
}
}
FirebaseMessaging.instance.onTokenRefresh.listen(
(token) => unawaited(updateToken(token)),
);
await updateToken();
return init;
}

View File

@@ -17,6 +17,8 @@ class TokenModel {
String? idToken; // Unique identifier for the token if needed
String? accessToken; // The main auth token
String? refreshToken; // Token used to refresh the access token
String? registrationId;
String? fcmToken;
DateTime? expiryDate;
int? tokenVersion;
int? updatedAtMs;

View File

@@ -1,5 +1,8 @@
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:logging/logging.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@@ -7,8 +10,34 @@ import 'package:firka/app/app_state.dart';
import 'package:firka/app/initialization.dart';
import 'package:firka/app/initialization_screen.dart';
Future<void> ertesites(String title, String message) async {
logger.info("REMOTE Title: $title");
logger.info("REMOTE Message: $message");
const NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
'teszt',
'Értesítés',
channelDescription: 'Értesítés a Krétától.',
importance: Importance.max,
ticker: 'ticker',
),
);
await flnp.show(
id: 0,
title: title,
body: message,
notificationDetails: notificationDetails,
payload: "szia",
);
}
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await ertesites(message.data["Title"], message.data["Message"]);
}
void main() async {
logger = Logger("Firka");
dio.options.connectTimeout = Duration(seconds: 5);
dio.options.receiveTimeout = Duration(seconds: 3);
dio.options.validateStatus = (status) => status != null && status < 500;
@@ -28,6 +57,26 @@ void main() async {
await setupLogging();
await Firebase.initializeApp(
name: defaultFirebaseAppName,
options: FirebaseOptions(
apiKey: "AIzaSyA_SnXigQkSvFuB5ECpgz8pZ1SjKzuKiFo",
appId: "1:694136934013:android:2d6873f63e005250",
androidClientId:
"694136934013-6e2jmrbqume6lt92d2ceb5se6uru4uvm.apps.googleusercontent.com",
projectId: "ellenorzo-v2",
messagingSenderId: "694136934013",
storageBucket: "ellenorzo-v2.appspot.com",
databaseURL: "https://ellenorzo-v2.firebaseio.com",
),
);
FirebaseMessaging.instance.setAutoInitEnabled(true);
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
runApp(InitializationScreen());
},
(error, stackTrace) {

View File

@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:collection';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firka/api/client/kreta_stream.dart';
import 'package:firka/ui/phone/widgets/info_card.dart';
import 'package:firka/ui/phone/widgets/lesson.dart';
@@ -41,6 +43,7 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
List<Lesson>? lessons;
List<NoticeBoardItem>? noticeBoard;
List<RemoteMessage> notifications = [];
List<InfoBoardItem>? infoBoard;
List<Test>? tests;
List<Grade>? grades;
@@ -189,6 +192,10 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
void initState() {
super.initState();
FirebaseMessaging.onMessage.listen((event) {
notifications.add(event);
}, onError: (e) => logger.info(e));
(() async {
await fetchData();
})();
@@ -216,6 +223,29 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
final omissionItems = omissions?.groupList((o) => o.date).values ?? [];
final noticeBoardWidgets = <(Widget, DateTime)>[];
for (final n in notifications) {
noticeBoardWidgets.add((
InfoCard.messageItem(
InfoBoardItem(
uid: "",
title: "Értesítés",
author: "Kréta",
type: NameUidDesc(
uid: "notificaiton",
name: "notification",
description: "notification",
),
contentHTML:
"<code>${n.data}</code><code>${n.notification}<br/>${n.notification?.title}<br/>${n.notification?.body}<br/>${n.notification?.android}",
contentText: "",
date: now,
createdAt: now,
),
),
now,
));
}
for (final item in infoItems) {
noticeBoardWidgets.add((InfoCard.messageItem(item), item.date));
}

View File

@@ -165,6 +165,8 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
await initializeApp();
await initData.client.updateFCMToken();
if (!mounted) return NavigationDecision.prevent;
if (mounted) {
@@ -286,8 +288,8 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
opacity: _isLoading
? 1.0
: _fadeAnimationController!.isAnimating
? _fadeAnimation!.value
: 0.0,
? _fadeAnimation!.value
: 0.0,
duration: const Duration(milliseconds: 300),
child: Container(
color: appStyle.colors.background,
@@ -332,7 +334,8 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
text: _displayPath,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
color: appStyle.colors.textTeritary ??
color:
appStyle.colors.textTeritary ??
appStyle.colors.textSecondary,
),
),

View File

@@ -1,6 +1,6 @@
name: firka
description: "Firka, Alternatív e-Kréta kliens."
publish_to: 'none'
publish_to: "none"
version: 1.1.2+2001
@@ -17,10 +17,11 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_launcher_icons: ^0.14.3
flutter_local_notifications: ^22.0.0
dio: ^5.8.0+1
isar_community: 3.3.0
isar_community_flutter_libs: 3.3.0
build_runner: any
isar_community: ^3.3.0
isar_community_flutter_libs: ^3.3.0
build_runner: ^2.15.0
path_provider: ^2.1.0
carousel_slider: ^5.0.0
webview_flutter: ^4.7.0
@@ -30,7 +31,7 @@ dependencies:
permission_handler: ^12.0.1
flutter_localizations:
sdk: flutter
intl: any
intl: ^0.20.2
image_picker: ^1.1.2
image: ^4.5.4
path: ^1.9.1
@@ -59,20 +60,22 @@ dependencies:
go_router: ^17.1.0
flutter_bloc: ^9.0.0
vibration: ^3.1.8
firebase_messaging: ^16.2.0
firebase_core: ^4.7.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
yaml: ^3.1.2
isar_community_generator: 3.3.0
isar_community_generator: ^3.3.0
android_notification_icons: ^0.0.1
integration_test:
sdk: flutter
android_notification_icons:
image_path: 'assets/images/logos/dave_monochrome.png'
icon_name: 'ic_notification'
image_path: "assets/images/logos/dave_monochrome.png"
icon_name: "ic_notification"
flutter:
generate: true