migrated custom timetable events to Marianum-Connect and refactored push notification handling
This commit is contained in:
@@ -13,22 +13,29 @@ import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
import 'telemetry_device_id.dart';
|
||||
|
||||
/// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) — one
|
||||
/// upsert per app start carrying the stable install id, platform, app version
|
||||
/// and device info. Bearer-authenticated via the shared dio interceptor.
|
||||
/// Replaces the legacy mhsl.eu `server/userIndex/update` call.
|
||||
/// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) —
|
||||
/// upserts the stable install id, platform, app version and device info. Sent
|
||||
/// once on app start and again once push registration completes that session
|
||||
/// (so a fresh registration isn't under-reported until the next launch).
|
||||
/// Bearer-authenticated via the shared dio interceptor. Replaces the legacy
|
||||
/// mhsl.eu `server/userIndex/update` call.
|
||||
class TelemetryHeartbeat {
|
||||
final Dio _dio;
|
||||
|
||||
TelemetryHeartbeat({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
/// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed
|
||||
/// send never disrupts app start. Used from the app shell's initState.
|
||||
static void report() {
|
||||
unawaited(TelemetryHeartbeat().send().catchError((Object _) {}));
|
||||
/// send never disrupts app start. Used from the app shell's initState and
|
||||
/// re-emitted once push registration completes (see `_MainState._syncPush`).
|
||||
static void report({required bool notificationsEnabled}) {
|
||||
unawaited(
|
||||
TelemetryHeartbeat()
|
||||
.send(notificationsEnabled: notificationsEnabled)
|
||||
.catchError((Object _) {}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> send() async {
|
||||
Future<void> send({required bool notificationsEnabled}) async {
|
||||
try {
|
||||
final info = DeviceInfoPlugin();
|
||||
final package = await PackageInfo.fromPlatform();
|
||||
@@ -58,7 +65,12 @@ class TelemetryHeartbeat {
|
||||
MarianumConnectEndpoint.resolve('me/telemetry'),
|
||||
data: {
|
||||
'deviceIdentifier': deviceIdentifier,
|
||||
// `pushDeviceIdentifier` reflects a *completed* registration and is
|
||||
// absent until it lands; `pushEnabled` carries the user's intent
|
||||
// (the notification toggle) so the backend can tell "user wants push"
|
||||
// apart from "registration not finished yet".
|
||||
'pushDeviceIdentifier': ?pushDeviceIdentifier,
|
||||
'pushEnabled': notificationsEnabled,
|
||||
'platform': platform,
|
||||
'appVersion': package.version,
|
||||
'appBuild': int.tryParse(package.buildNumber),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:localstore/localstore.dart';
|
||||
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../demo/demo_mode.dart';
|
||||
import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event.dart';
|
||||
import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart';
|
||||
import '../../../mhsl/custom_timetable_event/remove/remove_custom_timetable_event.dart';
|
||||
import '../../../mhsl/custom_timetable_event/remove/remove_custom_timetable_event_params.dart';
|
||||
import 'timetable_custom_events_add.dart';
|
||||
|
||||
/// One-time migration of the user's custom timetable events from the legacy
|
||||
/// MHSL backend to Marianum-Connect. Runs at most once per install, guarded by
|
||||
/// a persisted flag.
|
||||
///
|
||||
/// The MHSL identity is `sha512(username:password)` — computed here exactly as
|
||||
/// the app always did, so the fetch matches the user's own events perfectly.
|
||||
/// Each event is POSTed to Marianum-Connect and then deleted from MHSL, which
|
||||
/// makes the whole run idempotent and resumable: a re-run after a mid-way
|
||||
/// failure only sees the events that were not yet moved, so nothing is
|
||||
/// duplicated. The flag is set only once MHSL reports no remaining events.
|
||||
class CustomEventsMigration {
|
||||
static const String _collection = 'MarianumMobile';
|
||||
static const String _document = 'customEventsMigration';
|
||||
static const String _doneKey = 'migratedToMc';
|
||||
|
||||
const CustomEventsMigration._();
|
||||
|
||||
static Future<void> runOnce() async {
|
||||
if (DemoMode.active) return;
|
||||
if (await _isDone()) return;
|
||||
|
||||
try {
|
||||
final response = await GetCustomTimetableEvent(
|
||||
GetCustomTimetableEventParams(AccountData().getUserSecret()),
|
||||
).run();
|
||||
|
||||
for (final event in response.events) {
|
||||
await TimetableCustomEventsAdd().run(event);
|
||||
await RemoveCustomTimetableEvent(
|
||||
RemoveCustomTimetableEventParams(event.id),
|
||||
).run();
|
||||
}
|
||||
|
||||
await _markDone();
|
||||
log('Custom events migration: moved ${response.events.length} event(s) to Marianum-Connect.');
|
||||
} catch (e) {
|
||||
// Leave the flag unset so the next launch retries; the delete-after-post
|
||||
// above keeps a partial run duplicate-free.
|
||||
log('Custom events migration failed, will retry on next launch: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _isDone() async {
|
||||
final data = await Localstore.instance
|
||||
.collection(_collection)
|
||||
.doc(_document)
|
||||
.get();
|
||||
return data != null && data[_doneKey] == true;
|
||||
}
|
||||
|
||||
static Future<void> _markDone() async {
|
||||
await Localstore.instance.collection(_collection).doc(_document).set({
|
||||
_doneKey: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
|
||||
class TimetableCustomEventsAdd {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableCustomEventsAdd({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<void> run(CustomTimetableEvent event) async {
|
||||
try {
|
||||
await _dio.post<void>(
|
||||
MarianumConnectEndpoint.resolve('timetable/custom-events'),
|
||||
data: event.toJson(),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../../../request_cache.dart';
|
||||
import 'timetable_custom_events_get.dart';
|
||||
|
||||
class TimetableCustomEventsCache
|
||||
extends SimpleCache<GetCustomTimetableEventResponse> {
|
||||
TimetableCustomEventsCache({super.onUpdate, super.onError, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => TimetableCustomEventsGet().run(),
|
||||
fromJson: GetCustomTimetableEventResponse.fromJson,
|
||||
) {
|
||||
start('customTimetableEvents');
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
|
||||
class TimetableCustomEventsGet {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableCustomEventsGet({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<GetCustomTimetableEventResponse> run() async {
|
||||
try {
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('timetable/custom-events'),
|
||||
);
|
||||
return GetCustomTimetableEventResponse.fromJson(response.data!);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
|
||||
class TimetableCustomEventsRemove {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableCustomEventsRemove({Dio? dio})
|
||||
: _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<void> run(String id) async {
|
||||
try {
|
||||
await _dio.delete<void>(
|
||||
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
|
||||
class TimetableCustomEventsUpdate {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableCustomEventsUpdate({Dio? dio})
|
||||
: _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<void> run(String id, CustomTimetableEvent event) async {
|
||||
try {
|
||||
await _dio.put<void>(
|
||||
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
|
||||
data: event.toJson(),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../mhsl_api.dart';
|
||||
import 'add_custom_timetable_event_params.dart';
|
||||
|
||||
class AddCustomTimetableEvent extends MhslApi<void> {
|
||||
AddCustomTimetableEventParams params;
|
||||
|
||||
AddCustomTimetableEvent(this.params) : super('server/timetable/customEvents');
|
||||
|
||||
@override
|
||||
void assemble(String raw) {}
|
||||
|
||||
@override
|
||||
Future<Response>? request(Uri uri) {
|
||||
var body = jsonEncode(params.toJson());
|
||||
return http.post(uri, body: body);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../custom_timetable_event.dart';
|
||||
|
||||
part 'add_custom_timetable_event_params.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class AddCustomTimetableEventParams {
|
||||
String user;
|
||||
CustomTimetableEvent event;
|
||||
|
||||
AddCustomTimetableEventParams(this.user, this.event);
|
||||
|
||||
factory AddCustomTimetableEventParams.fromJson(Map<String, dynamic> json) =>
|
||||
_$AddCustomTimetableEventParamsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AddCustomTimetableEventParamsToJson(this);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'add_custom_timetable_event_params.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AddCustomTimetableEventParams _$AddCustomTimetableEventParamsFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => AddCustomTimetableEventParams(
|
||||
json['user'] as String,
|
||||
CustomTimetableEvent.fromJson(json['event'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AddCustomTimetableEventParamsToJson(
|
||||
AddCustomTimetableEventParams instance,
|
||||
) => <String, dynamic>{'user': instance.user, 'event': instance.event.toJson()};
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../mhsl_api.dart';
|
||||
|
||||
part 'custom_timetable_event.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -9,15 +7,23 @@ class CustomTimetableEvent {
|
||||
String id;
|
||||
String title;
|
||||
String description;
|
||||
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
|
||||
@JsonKey(toJson: _dateToJson, fromJson: _dateFromJson)
|
||||
DateTime startDate;
|
||||
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
|
||||
@JsonKey(toJson: _dateToJson, fromJson: _dateFromJson)
|
||||
DateTime endDate;
|
||||
String? color;
|
||||
String rrule;
|
||||
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
|
||||
|
||||
/// When true, occurrences of a recurring event that fall inside a school
|
||||
/// holiday are hidden in the calendar. Purely client-side: the backend only
|
||||
/// stores the flag, the exclusion is applied at render time against the
|
||||
/// holiday list. Ignored for non-recurring events (empty [rrule]).
|
||||
@JsonKey(defaultValue: false)
|
||||
bool skipHolidays;
|
||||
|
||||
@JsonKey(toJson: _dateToJson, fromJson: _dateFromJson)
|
||||
DateTime createdAt;
|
||||
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
|
||||
@JsonKey(toJson: _dateToJson, fromJson: _dateFromJson)
|
||||
DateTime updatedAt;
|
||||
|
||||
CustomTimetableEvent({
|
||||
@@ -28,6 +34,7 @@ class CustomTimetableEvent {
|
||||
required this.endDate,
|
||||
required this.color,
|
||||
required this.rrule,
|
||||
this.skipHolidays = false,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@@ -35,4 +42,13 @@ class CustomTimetableEvent {
|
||||
factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomTimetableEventFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this);
|
||||
|
||||
// Marianum-Connect serializes LocalDateTime as ISO-8601 (`yyyy-MM-ddTHH:mm:ss`)
|
||||
// and its GSON parser rejects anything else. DateTime.parse still reads the
|
||||
// old MHSL `yyyy-MM-dd HH:mm:ss` shape too, so events fetched during the
|
||||
// one-time migration deserialize unchanged.
|
||||
static DateTime _dateFromJson(String raw) => DateTime.parse(raw);
|
||||
static String _dateToJson(DateTime d) =>
|
||||
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'
|
||||
'T${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}:${d.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ CustomTimetableEvent _$CustomTimetableEventFromJson(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
startDate: MhslApi.dateTimeFromJson(json['startDate'] as String),
|
||||
endDate: MhslApi.dateTimeFromJson(json['endDate'] as String),
|
||||
startDate: CustomTimetableEvent._dateFromJson(json['startDate'] as String),
|
||||
endDate: CustomTimetableEvent._dateFromJson(json['endDate'] as String),
|
||||
color: json['color'] as String?,
|
||||
rrule: json['rrule'] as String,
|
||||
createdAt: MhslApi.dateTimeFromJson(json['createdAt'] as String),
|
||||
updatedAt: MhslApi.dateTimeFromJson(json['updatedAt'] as String),
|
||||
skipHolidays: json['skipHolidays'] as bool? ?? false,
|
||||
createdAt: CustomTimetableEvent._dateFromJson(json['createdAt'] as String),
|
||||
updatedAt: CustomTimetableEvent._dateFromJson(json['updatedAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomTimetableEventToJson(
|
||||
@@ -26,10 +27,11 @@ Map<String, dynamic> _$CustomTimetableEventToJson(
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'startDate': MhslApi.dateTimeToJson(instance.startDate),
|
||||
'endDate': MhslApi.dateTimeToJson(instance.endDate),
|
||||
'startDate': CustomTimetableEvent._dateToJson(instance.startDate),
|
||||
'endDate': CustomTimetableEvent._dateToJson(instance.endDate),
|
||||
'color': instance.color,
|
||||
'rrule': instance.rrule,
|
||||
'createdAt': MhslApi.dateTimeToJson(instance.createdAt),
|
||||
'updatedAt': MhslApi.dateTimeToJson(instance.updatedAt),
|
||||
'skipHolidays': instance.skipHolidays,
|
||||
'createdAt': CustomTimetableEvent._dateToJson(instance.createdAt),
|
||||
'updatedAt': CustomTimetableEvent._dateToJson(instance.updatedAt),
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import '../../../request_cache.dart';
|
||||
import 'get_custom_timetable_event.dart';
|
||||
import 'get_custom_timetable_event_params.dart';
|
||||
import 'get_custom_timetable_event_response.dart';
|
||||
|
||||
class GetCustomTimetableEventCache
|
||||
extends SimpleCache<GetCustomTimetableEventResponse> {
|
||||
GetCustomTimetableEventCache(
|
||||
GetCustomTimetableEventParams params, {
|
||||
super.onUpdate,
|
||||
super.onError,
|
||||
super.renew,
|
||||
}) : super(
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => GetCustomTimetableEvent(params).run(),
|
||||
fromJson: GetCustomTimetableEventResponse.fromJson,
|
||||
) {
|
||||
start('customTimetableEvents');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../mhsl_api.dart';
|
||||
import 'update_custom_timetable_event_params.dart';
|
||||
|
||||
class UpdateCustomTimetableEvent extends MhslApi<void> {
|
||||
UpdateCustomTimetableEventParams params;
|
||||
|
||||
UpdateCustomTimetableEvent(this.params)
|
||||
: super('server/timetable/customEvents');
|
||||
|
||||
@override
|
||||
void assemble(String raw) {}
|
||||
|
||||
@override
|
||||
Future<Response>? request(Uri uri) =>
|
||||
http.patch(uri, body: jsonEncode(params.toJson()));
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../custom_timetable_event.dart';
|
||||
|
||||
part 'update_custom_timetable_event_params.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class UpdateCustomTimetableEventParams {
|
||||
String id;
|
||||
CustomTimetableEvent event;
|
||||
|
||||
UpdateCustomTimetableEventParams(this.id, this.event);
|
||||
|
||||
factory UpdateCustomTimetableEventParams.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$UpdateCustomTimetableEventParamsFromJson(json);
|
||||
Map<String, dynamic> toJson() =>
|
||||
_$UpdateCustomTimetableEventParamsToJson(this);
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'update_custom_timetable_event_params.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UpdateCustomTimetableEventParams _$UpdateCustomTimetableEventParamsFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => UpdateCustomTimetableEventParams(
|
||||
json['id'] as String,
|
||||
CustomTimetableEvent.fromJson(json['event'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateCustomTimetableEventParamsToJson(
|
||||
UpdateCustomTimetableEventParams instance,
|
||||
) => <String, dynamic>{'id': instance.id, 'event': instance.event.toJson()};
|
||||
Reference in New Issue
Block a user