From aea12d20fc08afa6206b07ff0a887ac83df22e72 Mon Sep 17 00:00:00 2001 From: b3ni15 Date: Sat, 7 Mar 2026 12:13:23 +0100 Subject: [PATCH] Add Live Activity feature with consent dialog and settings integration --- .../lib/api/providers/live_card_provider.dart | 17 ++++ refilc/lib/database/init.dart | 17 ++++ refilc/lib/models/settings.dart | 17 +++- .../screens/navigation/navigation_screen.dart | 10 +++ .../live_activity_consent_dialog.dart | 81 +++++++++++++++++++ .../settings/settings_screen.i18n.dart | 15 ++++ .../settings/submenu/general_screen.dart | 58 +++++++++++++ 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 refilc_mobile_ui/lib/screens/settings/live_activity_consent_dialog.dart diff --git a/refilc/lib/api/providers/live_card_provider.dart b/refilc/lib/api/providers/live_card_provider.dart index a7cb8d0e..42cbf340 100644 --- a/refilc/lib/api/providers/live_card_provider.dart +++ b/refilc/lib/api/providers/live_card_provider.dart @@ -68,6 +68,10 @@ class LiveCardProvider extends ChangeNotifier { // Token figyelése: amikor az APNs tokent ad (első vagy rotation), regisztráljuk a szerverrel PlatformChannel.onTokenUpdated = (pushToken, deviceId, bundleId) { + if (!_settings.liveActivityEnabled) { + debugPrint("Push token érkezett, de Live Activity nincs engedélyezve - skip"); + return; + } debugPrint("Push token érkezett: $pushToken"); serverSync.registerAndSync( deviceId: deviceId, @@ -345,6 +349,16 @@ class LiveCardProvider extends ChangeNotifier { //LIVE ACTIVITIES + // Guard: if not enabled, stop any running activity and skip + if (!_settings.liveActivityEnabled) { + if (hasActivityStarted) { + debugPrint("Live Activity nincs engedélyezve, de fut – leállítás..."); + PlatformChannel.endLiveActivity(); + serverSync.unregister(); + hasActivityStarted = false; + } + } else { + //CREATE if (!hasActivityStarted && !hasUserDismissed && @@ -428,6 +442,9 @@ class LiveCardProvider extends ChangeNotifier { hasActivityStarted = false; hasUserDismissed = false; } + + } // end of liveActivityEnabled else block + LAData = toMap(); notifyListeners(); } diff --git a/refilc/lib/database/init.dart b/refilc/lib/database/init.dart index 0f68e20e..e51e91f4 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_print +import 'dart:convert'; import 'dart:io'; import 'package:refilc/api/providers/database_provider.dart'; @@ -57,6 +58,7 @@ const settingsDB = DatabaseStruct("settings", { "uwu_mode": int, "new_popups": int, "unseen_new_features": String, + "live_activity_enabled": int, "cloud_sync_enabled": int, "cloud_sync_token": String, "local_updated_at": String, @@ -185,6 +187,21 @@ Future initDB(DatabaseProvider database) async { print("ERROR: migrateDB: $error"); } + // One-time migration: add 'live_activity_consent' to unseen_new_features for existing users + try { + final rows = await db.query('settings', columns: ['unseen_new_features']); + if (rows.isNotEmpty) { + final raw = rows.first['unseen_new_features'] as String? ?? '[]'; + final list = (jsonDecode(raw) as List).cast(); + if (!list.contains('live_activity_consent')) { + list.add('live_activity_consent'); + await db.update('settings', {'unseen_new_features': jsonEncode(list)}); + } + } + } catch (e) { + print("ERROR: live_activity_consent migration: $e"); + } + return db; } diff --git a/refilc/lib/models/settings.dart b/refilc/lib/models/settings.dart index f4c4e3a1..9fa05d10 100644 --- a/refilc/lib/models/settings.dart +++ b/refilc/lib/models/settings.dart @@ -110,6 +110,7 @@ class SettingsProvider extends ChangeNotifier { bool _uwuMode; bool _newPopups; List _unseenNewFeatures; + bool _liveActivityEnabled; bool _cloudSyncEnabled; String _cloudSyncToken; DateTime _updatedAt; @@ -189,6 +190,7 @@ class SettingsProvider extends ChangeNotifier { required bool uwuMode, required bool newPopups, required List unseenNewFeatures, + required bool liveActivityEnabled, required bool cloudSyncEnabled, required String cloudSyncToken, required DateTime updatedAt, @@ -265,6 +267,7 @@ class SettingsProvider extends ChangeNotifier { _uwuMode = uwuMode, _newPopups = newPopups, _unseenNewFeatures = unseenNewFeatures, + _liveActivityEnabled = liveActivityEnabled, _cloudSyncEnabled = cloudSyncEnabled, _cloudSyncToken = cloudSyncToken, _updatedAt = updatedAt, @@ -360,6 +363,7 @@ class SettingsProvider extends ChangeNotifier { uwuMode: map['uwu_mode'] == 1, newPopups: map['new_popups'] == 1, unseenNewFeatures: jsonDecode(map["unseen_new_features"]).cast(), + liveActivityEnabled: map['live_activity_enabled'] == 1, cloudSyncEnabled: map['cloud_sync_enabled'] == 1, cloudSyncToken: map['cloud_sync_token'], updatedAt: DateTime.tryParse(map['local_updated_at']) ?? DateTime.now(), @@ -443,6 +447,7 @@ class SettingsProvider extends ChangeNotifier { "uwu_mode": _uwuMode ? 1 : 0, "new_popups": _newPopups ? 1 : 0, "unseen_new_features": jsonEncode(_unseenNewFeatures), + "live_activity_enabled": _liveActivityEnabled ? 1 : 0, "cloud_sync_enabled": _cloudSyncEnabled ? 1 : 0, "cloud_sync_token": _cloudSyncToken, "local_updated_at": _updatedAt.toIso8601String(), @@ -529,7 +534,8 @@ class SettingsProvider extends ChangeNotifier { newColors: true, uwuMode: false, newPopups: false, - unseenNewFeatures: [], + unseenNewFeatures: ['live_activity_consent'], + liveActivityEnabled: false, cloudSyncEnabled: false, cloudSyncToken: '', updatedAt: DateTime.now(), @@ -608,6 +614,7 @@ class SettingsProvider extends ChangeNotifier { bool get uwuMode => _uwuMode; bool get newPopups => _newPopups; List get unseenNewFeatures => _unseenNewFeatures; + bool get liveActivityEnabled => _liveActivityEnabled; bool get cloudSyncEnabled => _cloudSyncEnabled; String get cloudSyncToken => _cloudSyncToken; DateTime get updatedAt => _updatedAt; @@ -683,6 +690,7 @@ class SettingsProvider extends ChangeNotifier { bool? uwuMode, bool? newPopups, List? unseenNewFeatures, + bool? liveActivityEnabled, bool? cloudSyncEnabled, String? cloudSyncToken, bool? qTimetableLessonNum, @@ -886,6 +894,12 @@ class SettingsProvider extends ChangeNotifier { if (unseenNewFeatures != null && unseenNewFeatures != _unseenNewFeatures) { _unseenNewFeatures = unseenNewFeatures; } + if (liveActivityEnabled != null && liveActivityEnabled != _liveActivityEnabled) { + _liveActivityEnabled = liveActivityEnabled; + if (!liveActivityEnabled && Platform.isIOS) { + LiveCardProvider.hasActivitySettingsChanged = true; + } + } if (cloudSyncEnabled != null && cloudSyncEnabled != _cloudSyncEnabled) { _cloudSyncEnabled = cloudSyncEnabled; } @@ -1005,6 +1019,7 @@ class SettingsProvider extends ChangeNotifier { newPopups: map['new_popups'] == 1, unseenNewFeatures: jsonDecode(map["unseen_new_features"] ?? "[]").cast(), + liveActivityEnabled: map['live_activity_enabled'] == 1, cloudSyncEnabled: map['cloud_sync_enabled'] == 1, cloudSyncToken: map['cloud_sync_token'], qTimetableLessonNum: map['q_timetable_lesson_num'] == 1, diff --git a/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart b/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart index 2390c50e..03952f52 100644 --- a/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart +++ b/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart @@ -18,6 +18,7 @@ import 'package:refilc_mobile_ui/screens/navigation/navigation_route_handler.dar import 'package:refilc_mobile_ui/screens/navigation/status_bar.dart'; import 'package:refilc_mobile_ui/screens/news/news_view.dart'; import 'package:refilc_mobile_ui/screens/settings/settings_screen.dart'; +import 'package:refilc_mobile_ui/screens/settings/live_activity_consent_dialog.dart'; import 'package:refilc_plus/ui/mobile/goal_planner/goal_complete_modal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -32,6 +33,7 @@ import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart'; import 'package:background_fetch/background_fetch.dart'; import 'package:refilc_plus/providers/goal_provider.dart'; import 'package:refilc/api/providers/ad_provider.dart'; +import 'dart:io' show Platform; class NavigationScreen extends StatefulWidget { const NavigationScreen({super.key}); @@ -190,6 +192,14 @@ class NavigationScreenState extends State // initial sync syncAll(context); setupQuickActions(); + + // Show live activity consent dialog on iOS + if (Platform.isIOS && + settings.unseenNewFeatures.contains('live_activity_consent')) { + WidgetsBinding.instance.addPostFrameCallback((_) { + LiveActivityConsentDialog.show(context); + }); + } } @override diff --git a/refilc_mobile_ui/lib/screens/settings/live_activity_consent_dialog.dart b/refilc_mobile_ui/lib/screens/settings/live_activity_consent_dialog.dart new file mode 100644 index 00000000..6baf6af8 --- /dev/null +++ b/refilc_mobile_ui/lib/screens/settings/live_activity_consent_dialog.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; + +class LiveActivityConsentDialog extends StatelessWidget { + const LiveActivityConsentDialog({super.key}); + + static Future show(BuildContext context) => showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const LiveActivityConsentDialog(), + ); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0), + child: Material( + borderRadius: BorderRadius.circular(12.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + "live_activity_consent_title".i18n, + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Text( + "live_activity_consent_body".i18n, + style: const TextStyle(fontSize: 14.0), + ), + ), + ), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => _respond(context, false), + child: Text("live_activity_decline".i18n), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: FilledButton( + onPressed: () => _respond(context, true), + child: Text("live_activity_accept".i18n), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + void _respond(BuildContext context, bool accepted) { + final settings = Provider.of(context, listen: false); + final unseen = + List.from(settings.unseenNewFeatures); + unseen.remove('live_activity_consent'); + settings.update( + liveActivityEnabled: accepted, + unseenNewFeatures: unseen, + ); + Navigator.of(context).pop(); + } +} diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart index 9d45b3e2..1b2ee395 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart @@ -98,6 +98,11 @@ extension SettingsLocalization on String { "rename_subjects": "Rename Subjects", "rename_teachers": "Rename Teachers", "show_breaks": "Show Breaks", + "live_activity_enabled": "Live Activity", + "live_activity_consent_title": "Live Activity – Data Privacy", + "live_activity_consent_body": "The Live Activity feature sends data to our server for real-time push notification updates.\n\nData sent:\n• Device identifier\n• APNs push token\n• Your timetable\n\nPurpose:\n• Real-time Live Activity updates via push notifications\n\nData is stored on an encrypted server. You can disable this at any time in Settings > General.", + "live_activity_accept": "Allow", + "live_activity_decline": "Don't Allow", "fonts": "Fonts", "font_family": "Font Family", "calendar_sync": "Calendar Sync", @@ -228,6 +233,11 @@ extension SettingsLocalization on String { "rename_subjects": "Tantárgyak átnevezése", "rename_teachers": "Tanárok átnevezése", "show_breaks": "Szünetek megjelenítése", + "live_activity_enabled": "Live Activity", + "live_activity_consent_title": "Live Activity – Adatvédelem", + "live_activity_consent_body": "A Live Activity funkció adatokat küld a szerverünkre a valós idejű push értesítés frissítésekhez.\n\nKüldött adatok:\n• Eszközazonosító\n• APNs push token\n• Órarendjeid\n\nCél:\n• Valós idejű Live Activity frissítések push értesítéssel\n\nAz adatokat titkosított szerveren tároljuk. Bármikor kikapcsolhatod a Beállítások > Általános menüben.", + "live_activity_accept": "Engedélyezem", + "live_activity_decline": "Nem engedélyezem", "fonts": "Betűk", "font_family": "Betűtípus", "calendar_sync": "Naptár szinkronizálás", @@ -358,6 +368,11 @@ extension SettingsLocalization on String { "rename_subjects": "Fächer umbenennen", "rename_teachers": "Lehrer umbenennen", "show_breaks": "Pausen anzeigen", + "live_activity_enabled": "Live Activity", + "live_activity_consent_title": "Live Activity – Datenschutz", + "live_activity_consent_body": "Die Live Activity-Funktion sendet Daten an unseren Server für Echtzeit-Push-Benachrichtigungen.\n\nGesendete Daten:\n• Gerätekennung\n• APNs Push-Token\n• Ihr Stundenplan\n\nZweck:\n• Echtzeit Live Activity-Updates über Push-Benachrichtigungen\n\nDaten werden auf einem verschlüsselten Server gespeichert. Sie können dies jederzeit unter Einstellungen > Allgemein deaktivieren.", + "live_activity_accept": "Erlauben", + "live_activity_decline": "Nicht erlauben", "fonts": "Schriftarten", "font_family": "Schriftfamilie", "calendar_sync": "Kalender-Synchronisation", diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/general_screen.dart b/refilc_mobile_ui/lib/screens/settings/submenu/general_screen.dart index a67547cc..076dd20a 100644 --- a/refilc_mobile_ui/lib/screens/settings/submenu/general_screen.dart +++ b/refilc_mobile_ui/lib/screens/settings/submenu/general_screen.dart @@ -1,3 +1,6 @@ +import 'dart:io' show Platform; +import 'package:refilc/api/providers/liveactivity/platform_channel.dart'; +import 'package:refilc/api/providers/live_card_provider.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc/utils/format.dart'; @@ -123,6 +126,61 @@ class GeneralSettingsScreenState extends State { ), ], ), + if (Platform.isIOS) + SplittedPanel( + padding: const EdgeInsets.only(top: 9.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () { + final newVal = !settingsProvider.liveActivityEnabled; + settingsProvider.update(liveActivityEnabled: newVal); + if (!newVal) { + PlatformChannel.endLiveActivity(); + LiveCardProvider.serverSync.unregister(); + LiveCardProvider.hasActivityStarted = false; + } + setState(() {}); + }, + title: Text( + "live_activity_enabled".i18n, + style: TextStyle( + color: AppColors.of(context).text.withValues( + alpha: settingsProvider.liveActivityEnabled + ? .95 + : .25), + ), + ), + leading: Icon( + FeatherIcons.activity, + size: 22.0, + color: AppColors.of(context).text.withValues( + alpha: settingsProvider.liveActivityEnabled + ? .95 + : .25), + ), + trailing: Switch( + onChanged: (v) { + settingsProvider.update(liveActivityEnabled: v); + if (!v) { + PlatformChannel.endLiveActivity(); + LiveCardProvider.serverSync.unregister(); + LiveCardProvider.hasActivityStarted = false; + } + setState(() {}); + }, + value: settingsProvider.liveActivityEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), SplittedPanel( padding: const EdgeInsets.only(top: 9.0), cardPadding: const EdgeInsets.all(4.0),