- Fixed user switching so previous user data is now properly cleared.

- Fixed re-login behavior: after every new login, the Live Activities privacy notice is shown as intended.
- Made the Live Activity toggle user-specific to prevent settings from carrying over after switching accounts.
- Fixed user logout so the backend now correctly removes all related data.
- Fixed first-install behavior so the Live Activities privacy notice no longer appears on the public beta screen; it now only appears after reaching the home page.
This commit is contained in:
Horváth Gergely
2025-11-25 14:18:23 +01:00
parent fe70fc7bd1
commit 8c4bbd0905
4 changed files with 208 additions and 49 deletions

View File

@@ -28,6 +28,92 @@ class LiveActivityService {
static String? _cachedDeviceToken;
static bool _isInitialized = false;
/// Get current user's studentId for user-specific settings
static String? _getCurrentStudentId() {
try {
if (!initDone || initData.client == null || initData.client.model == null) {
return null;
}
return initData.client.model.studentId;
} catch (e) {
_logger.warning('Error getting current studentId: $e');
return null;
}
}
/// Get user-specific Live Activity enabled state from SharedPreferences
static Future<bool> _getUserLiveActivityEnabled() async {
final studentId = _getCurrentStudentId();
if (studentId == null) return false;
final prefs = await SharedPreferences.getInstance();
final key = 'live_activity_enabled_$studentId';
return prefs.getBool(key) ?? false;
}
/// Set user-specific Live Activity enabled state to SharedPreferences
static Future<void> _setUserLiveActivityEnabled(bool value) async {
final studentId = _getCurrentStudentId();
if (studentId == null) return;
final prefs = await SharedPreferences.getInstance();
final key = 'live_activity_enabled_$studentId';
await prefs.setBool(key, value);
_logger.info('Saved LiveActivity enabled=$value for user $studentId');
}
/// Get user-specific privacy declined state from SharedPreferences
static Future<bool> _getUserPrivacyEverDeclined() async {
final studentId = _getCurrentStudentId();
if (studentId == null) return false;
final prefs = await SharedPreferences.getInstance();
final key = 'live_activity_privacy_ever_declined_$studentId';
return prefs.getBool(key) ?? false;
}
/// Set user-specific privacy declined state to SharedPreferences
static Future<void> _setUserPrivacyEverDeclined(bool value) async {
final studentId = _getCurrentStudentId();
if (studentId == null) return;
final prefs = await SharedPreferences.getInstance();
final key = 'live_activity_privacy_ever_declined_$studentId';
await prefs.setBool(key, value);
_logger.info('Saved privacy ever declined=$value for user $studentId');
}
/// Sync global setting with current user's setting
/// This ensures the Settings UI shows the correct state for the current user
static Future<void> syncGlobalSettingWithCurrentUser() async {
if (!Platform.isIOS) return;
try {
final studentId = _getCurrentStudentId();
if (studentId == null) {
_logger.warning('Cannot sync global setting: no current user');
return;
}
final userEnabled = await _getUserLiveActivityEnabled();
final globalSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_enabled"] as SettingsBoolean;
if (globalSetting.value != userEnabled) {
globalSetting.value = userEnabled;
await initData.isar.writeTxn(() async {
await globalSetting.save(initData.isar.appSettingsModels);
});
globalUpdate.update();
_logger.info('Global LiveActivity setting synced with user setting: $userEnabled for user $studentId');
}
} catch (e) {
_logger.warning('Error syncing global setting: $e');
}
}
/// Get current language code from settings
static String? _getCurrentLanguageCode() {
try {
@@ -115,14 +201,7 @@ class LiveActivityService {
/// Check if LiveActivity is enabled in settings
static Future<bool> isEnabled([SettingsStore? settingsStore]) async {
try {
if (settingsStore == null) {
return false;
}
final enabled = settingsStore
.group("settings")
.subGroup("application")["live_activity_enabled"] as SettingsBoolean;
return enabled.value;
return await _getUserLiveActivityEnabled();
} catch (e) {
_logger.warning('Error reading LiveActivity setting: $e');
return false;
@@ -135,18 +214,19 @@ class LiveActivityService {
if (!Platform.isIOS) return;
try {
final enabledSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_enabled"]
as SettingsBoolean;
final studentId = _getCurrentStudentId();
if (studentId == null) {
_logger.warning('Cannot change LiveActivity state: no current user');
return;
}
if (!enabled) {
await onUserLogout();
enabledSetting.value = false;
await initData.isar.writeTxn(() async {
await enabledSetting.save(initData.isar.appSettingsModels);
});
globalUpdate.update();
await _setUserLiveActivityEnabled(false);
await syncGlobalSettingWithCurrentUser();
_logger.info('LiveActivity disabled and user data cleared.');
} else {
_logger.info('Showing privacy consent screen (manual: $isManual)');
@@ -155,11 +235,9 @@ class LiveActivityService {
if (accepted == true) {
_logger.info('User accepted privacy policy');
enabledSetting.value = true;
await initData.isar.writeTxn(() async {
await enabledSetting.save(initData.isar.appSettingsModels);
});
globalUpdate.update();
await _setUserLiveActivityEnabled(true);
await syncGlobalSettingWithCurrentUser();
final studentResp = await initData.client.getStudent();
final studentName = studentResp.response?.name ?? initData.tokens.first.studentId ?? "Student";
@@ -172,19 +250,10 @@ class LiveActivityService {
} else {
_logger.info('User declined privacy policy or swiped back');
final everDeclinedSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_privacy_ever_declined"]
as SettingsBoolean;
await _setUserLiveActivityEnabled(false);
await _setUserPrivacyEverDeclined(true);
enabledSetting.value = false;
everDeclinedSetting.value = true;
await initData.isar.writeTxn(() async {
await enabledSetting.save(initData.isar.appSettingsModels);
await everDeclinedSetting.save(initData.isar.appSettingsModels);
});
globalUpdate.update();
await syncGlobalSettingWithCurrentUser();
}
}
} catch (e) {
@@ -198,17 +267,22 @@ class LiveActivityService {
if (!Platform.isIOS) return;
try {
final enabledSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_enabled"] as SettingsBoolean;
final studentId = _getCurrentStudentId();
if (studentId == null) {
_logger.warning('Cannot check consent screen: no current user');
return;
}
final everDeclinedSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_privacy_ever_declined"] as SettingsBoolean;
await syncGlobalSettingWithCurrentUser();
if (!enabledSetting.value && !everDeclinedSetting.value) {
final enabled = await _getUserLiveActivityEnabled();
final everDeclined = await _getUserPrivacyEverDeclined();
if (!enabled && !everDeclined) {
_logger.info('First use or new user - showing privacy consent automatically');
await handleEnabledChange(true, isManual: false);
} else {
_logger.info('User already has LiveActivity setting: enabled=$enabled, declined=$everDeclined');
}
} catch (e) {
_logger.warning('Error checking if consent screen needed: $e');
@@ -277,6 +351,16 @@ class LiveActivityService {
}
}
/// Get next Monday's date (or this Monday if today is Monday and it's early morning)
static DateTime _getNextMonday(DateTime now) {
final int daysUntilMonday = ((DateTime.monday - now.weekday) % 7);
final int daysToAdd = daysUntilMonday == 0 ? 7 : daysUntilMonday;
final nextMonday = now.add(Duration(days: daysToAdd));
return DateTime(nextMonday.year, nextMonday.month, nextMonday.day);
}
/// Called when user logs in successfully
/// Registers the device and uploads the *full* timetable
static Future<void> onUserLogin({
@@ -284,30 +368,82 @@ class LiveActivityService {
required String studentName,
SettingsStore? settingsStore,
}) async {
_logger.info('onUserLogin: Function called for $studentName');
if (!Platform.isIOS || !_isInitialized) {
_logger.warning('onUserLogin: Returning early - Platform.isIOS=${Platform.isIOS}, _isInitialized=$_isInitialized');
return;
}
final enabled = await isEnabled(settingsStore);
_logger.info('onUserLogin: LiveActivity enabled=$enabled');
if (!enabled) {
_logger.warning('onUserLogin: LiveActivity not enabled, returning early');
return;
}
try {
_logger.info('onUserLogin: Starting timetable fetch');
final now = DateTime.now();
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
_logger.info('onUserLogin: Fetching timetable from $startOfWeek to $endOfWeek');
final timetableResponse = await client.getTimeTable(startOfWeek, endOfWeek);
final allLessons = timetableResponse.response ?? [];
final allLessons = List<Lesson>.from(timetableResponse.response ?? []);
_logger.info('onUserLogin: Fetched ${allLessons.length} lessons for current week');
if (allLessons.isEmpty) {
_logger.warning('onUserLogin: No lessons found, returning early');
return;
}
final nextMonday = _getNextMonday(now);
final nextMondayEndOfDay = nextMonday.add(const Duration(days: 1));
_logger.info('Fetching next Monday timetable from $nextMonday to $nextMondayEndOfDay');
try {
final nextMondayTimetable = await client.getTimeTable(nextMonday, nextMondayEndOfDay);
final nextMondayLessons = nextMondayTimetable.response ?? [];
_logger.info('Fetched ${nextMondayLessons.length} lessons for next Monday');
if (nextMondayLessons.isNotEmpty) {
nextMondayLessons.sort((a, b) => a.start.compareTo(b.start));
final firstLesson = nextMondayLessons.first;
final notificationLesson = Lesson(
uid: '${firstLesson.uid}__FOR_NOTIFICATION_ONLY',
date: firstLesson.date,
start: firstLesson.start,
end: firstLesson.end,
name: firstLesson.name,
lessonNumber: firstLesson.lessonNumber,
teacher: firstLesson.teacher,
theme: firstLesson.theme,
roomName: firstLesson.roomName,
substituteTeacher: firstLesson.substituteTeacher,
type: firstLesson.type,
state: firstLesson.state,
canStudentEditHomework: firstLesson.canStudentEditHomework,
isHomeworkComplete: firstLesson.isHomeworkComplete,
attachments: firstLesson.attachments,
isDigitalLesson: firstLesson.isDigitalLesson,
digitalSupportDeviceTypeList: firstLesson.digitalSupportDeviceTypeList,
createdAt: firstLesson.createdAt ?? firstLesson.lastModifiedAt ?? DateTime.now(),
lastModifiedAt: firstLesson.lastModifiedAt,
);
allLessons.add(notificationLesson);
_logger.info('Added next Monday first lesson for notification: ${firstLesson.name} at ${firstLesson.start}');
}
} catch (e) {
_logger.warning('Could not fetch next Monday timetable for notification: $e');
}
final deviceToken = await _getOrWaitDeviceToken();
if (deviceToken == null) {
@@ -395,18 +531,29 @@ class LiveActivityService {
/// Called when user logs out
static Future<void> onUserLogout() async {
if (!Platform.isIOS) return;
_logger.info('onUserLogout: Function called');
if (!Platform.isIOS) {
_logger.warning('onUserLogout: Not iOS, returning early');
return;
}
try {
_logger.info('onUserLogout: Ending all activities');
await LiveActivityManager.endAllActivities();
final deviceToken = _cachedDeviceToken ?? await LiveActivityManager.getDeviceToken();
_logger.info('onUserLogout: Device token = ${deviceToken?.substring(0, 10)}...');
if (deviceToken != null) {
_logger.info('onUserLogout: Unregistering device from backend');
await _backendClient.unregisterDevice(deviceToken: deviceToken);
}
_logger.info('onUserLogout: Clearing cache');
await _clearCache();
_logger.info('onUserLogout: Stopping timetable monitoring');
_stopTimetableMonitoring();
_logger.info('User logout processed for LiveActivity');
@@ -434,7 +581,7 @@ class LiveActivityService {
final endOfWeek = startOfWeek.add(const Duration(days: 6));
final timetableResponse = await client.getTimeTable(startOfWeek, endOfWeek);
List<Lesson> allLessons = timetableResponse.response ?? [];
List<Lesson> allLessons = List<Lesson>.from(timetableResponse.response ?? []);
final nextMonday = endOfWeek.add(const Duration(days: 1));
final nextMondayEnd = nextMonday.add(const Duration(days: 1));

View File

@@ -132,13 +132,15 @@ class SettingsStore {
false,
isIOS,
() async {
final setting = initData.settings
final globalSetting = initData.settings
.group("settings")
.subGroup("application")["live_activity_enabled"] as SettingsBoolean;
final enabled = setting.value;
final enabled = globalSetting.value;
await LiveActivityService.handleEnabledChange(enabled, isManual: true);
await LiveActivityService.syncGlobalSettingWithCurrentUser();
}),
"live_activity_privacy_ever_declined": SettingsBoolean(
liveActivityPrivacyEverDeclined,

View File

@@ -274,8 +274,8 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
prefetch();
_preloadImages();
if (Platform.isIOS) {
Future.delayed(Duration(seconds: 2), () async {
if (Platform.isIOS && widget.data.settings.group("settings").boolean("beta_warning")) {
Future.delayed(Duration(seconds: 5), () async {
await LiveActivityService.showConsentScreenIfNeeded();
});
}

View File

@@ -23,6 +23,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../../../../helpers/firka_bundle.dart';
import '../../../../helpers/firka_state.dart';
import '../../../../helpers/settings.dart';
import '../../../../helpers/live_activity_service.dart';
import '../../widgets/login_webview.dart';
class SettingsScreen extends StatefulWidget {
@@ -640,6 +641,10 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
),
onTap: () async {
if (i != item.accountIndex) {
if (Platform.isIOS) {
await LiveActivityService.onUserLogout();
}
await widget.data.isar.writeTxn(() async {
item.accountIndex = i;
@@ -693,6 +698,10 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
)
]),
onTap: () async {
if (Platform.isIOS) {
await LiveActivityService.onUserLogout();
}
final active = widget.data.client.model.studentIdNorm!;
await widget.data.isar.writeTxn(() async {
@@ -702,6 +711,7 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
await item.save(widget.data.isar.appSettingsModels);
});
final accounts =
await widget.data.isar.tokenModels.where().findAll();