From e72ead7f5cb7a8bf1d2f64467cbbe75014b0fd8b Mon Sep 17 00:00:00 2001 From: K-w-e Date: Tue, 6 Feb 2024 19:26:13 +0100 Subject: [PATCH 1/5] WIP notifications system --- lib/main.dart | 12 ++- .../notifications/notifications_service.dart | 22 +++++ .../notifications/notifications_settings.dart | 75 ++++++++++++---- .../widgets/NotificationTypeTile.dart | 89 +++++++++++++++++++ lib/providers/settings_provider.dart | 13 ++- lib/utils/worker_manager.dart | 45 ++++++++++ pubspec.yaml | 3 + 7 files changed, 239 insertions(+), 20 deletions(-) create mode 100644 lib/pages/notifications/notifications_service.dart create mode 100644 lib/pages/notifications/widgets/NotificationTypeTile.dart create mode 100644 lib/utils/worker_manager.dart diff --git a/lib/main.dart b/lib/main.dart index 204bb672..4efc3dd4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:sossoldi/utils/worker_manager.dart'; +import 'pages/notifications/notifications_service.dart'; import 'providers/theme_provider.dart'; import 'routes.dart'; import 'utils/app_theme.dart'; -void main() { - initializeDateFormatting('it_IT', null) - .then((_) => runApp(const ProviderScope(child: Launcher()))); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + requestNotificationPermissions(); + initializeNotifications(); + Workmanager().initialize(callbackDispatcher, isInDebugMode: true); + initializeDateFormatting('it_IT', null).then((_) => runApp(const ProviderScope(child: Launcher()))); } class Launcher extends ConsumerWidget { diff --git a/lib/pages/notifications/notifications_service.dart b/lib/pages/notifications/notifications_service.dart new file mode 100644 index 00000000..668a19eb --- /dev/null +++ b/lib/pages/notifications/notifications_service.dart @@ -0,0 +1,22 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:permission_handler/permission_handler.dart'; + +final FlutterLocalNotificationsPlugin notificationsPlugin = FlutterLocalNotificationsPlugin(); + +const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'sossoldi#reminder', // id del canale + 'Reminder', // nome del canale + importance: Importance.high, +); + +void initializeNotifications() async { + var initializationSettingsAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher'); + var initializationSettingsIOS = const DarwinInitializationSettings(); + var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS); + notificationsPlugin.initialize(initializationSettings); + await notificationsPlugin.resolvePlatformSpecificImplementation()?.createNotificationChannel(channel); +} + +void requestNotificationPermissions() async { + await Permission.notification.request(); +} diff --git a/lib/pages/notifications/notifications_settings.dart b/lib/pages/notifications/notifications_settings.dart index e85b7dcf..e78d0428 100644 --- a/lib/pages/notifications/notifications_settings.dart +++ b/lib/pages/notifications/notifications_settings.dart @@ -2,22 +2,34 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../constants/functions.dart'; import '../../constants/style.dart'; import '../../providers/settings_provider.dart'; +import '../../utils/worker_manager.dart'; +import 'widgets/NotificationTypeTile.dart'; class NotificationsSettings extends ConsumerStatefulWidget { const NotificationsSettings({super.key}); @override - ConsumerState createState() => _NotificationsSettingsState(); + ConsumerState createState() => + _NotificationsSettingsState(); } -class _NotificationsSettingsState extends ConsumerState with Functions { +class _NotificationsSettingsState extends ConsumerState { + + void setNotificationTypeCallback(NotificationReminderType type) { + setState(() { + ref.watch(transactionReminderCadenceProvider.notifier).state = type; + ref.read(settingsProvider.notifier).updateNotifications(); + scheduleTransactionReminder(type); + }); + } + @override Widget build(BuildContext context) { - // Is used to init the state of the switches - ref.read(settingsProvider); + final isReminderEnabled = ref.watch(transactionReminderSwitchProvider); + final notificationSettings = ref.watch(settingsProvider); + return Scaffold( appBar: AppBar( leading: IconButton( @@ -30,7 +42,8 @@ class _NotificationsSettingsState extends ConsumerState w child: Column( children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), + padding: + const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), child: Row( children: [ Container( @@ -74,21 +87,40 @@ class _NotificationsSettingsState extends ConsumerState w ), ), CupertinoSwitch( - value: ref.watch(transactionReminderSwitchProvider), + value: isReminderEnabled, onChanged: (value) { - ref.read(transactionReminderSwitchProvider.notifier).state = value; + ref + .read(transactionReminderSwitchProvider.notifier) + .state = value; ref.read(settingsProvider.notifier).updateNotifications(); }, ), ], ), ), + AnimatedCrossFade( + crossFadeState: isReminderEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 150), + firstChild: Container(), + secondChild: Column( + children: [ + NotificationTypeTile(type: NotificationReminderType.daily, setNotificationTypeCallback: () => setNotificationTypeCallback(NotificationReminderType.daily)), + NotificationTypeTile(type: NotificationReminderType.weekly, setNotificationTypeCallback: () => setNotificationTypeCallback(NotificationReminderType.weekly)), + NotificationTypeTile(type: NotificationReminderType.monthly, setNotificationTypeCallback: () => setNotificationTypeCallback(NotificationReminderType.monthly)) + ], + ), + ), Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 28, top: 24, bottom: 8), child: Text( "RECURRING TRANSACTIONS", - style: Theme.of(context).textTheme.labelLarge!.copyWith(color: grey1), + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: grey1), ), ), Container( @@ -113,8 +145,12 @@ class _NotificationsSettingsState extends ConsumerState w CupertinoSwitch( value: ref.watch(transactionRecAddedSwitchProvider), onChanged: (value) { - ref.read(transactionRecAddedSwitchProvider.notifier).state = value; - ref.read(settingsProvider.notifier).updateNotifications(); + ref + .read(transactionRecAddedSwitchProvider.notifier) + .state = value; + ref + .read(settingsProvider.notifier) + .updateNotifications(); }, ), ], @@ -122,7 +158,8 @@ class _NotificationsSettingsState extends ConsumerState w const SizedBox(height: 12), Divider( height: 1, - color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + color: + Theme.of(context).colorScheme.primary.withOpacity(0.4), ), const SizedBox(height: 12), Row( @@ -137,8 +174,13 @@ class _NotificationsSettingsState extends ConsumerState w CupertinoSwitch( value: ref.watch(transactionRecReminderSwitchProvider), onChanged: (value) { - ref.read(transactionRecReminderSwitchProvider.notifier).state = value; - ref.read(settingsProvider.notifier).updateNotifications(); + ref + .read( + transactionRecReminderSwitchProvider.notifier) + .state = value; + ref + .read(settingsProvider.notifier) + .updateNotifications(); }, ), ], @@ -151,7 +193,10 @@ class _NotificationsSettingsState extends ConsumerState w padding: const EdgeInsets.only(left: 28, top: 6), child: Text( "Remind me before a recurring transaction is added", - style: Theme.of(context).textTheme.labelMedium!.copyWith(color: grey1), + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: grey1), ), ), ], diff --git a/lib/pages/notifications/widgets/NotificationTypeTile.dart b/lib/pages/notifications/widgets/NotificationTypeTile.dart new file mode 100644 index 00000000..2a25d558 --- /dev/null +++ b/lib/pages/notifications/widgets/NotificationTypeTile.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../providers/settings_provider.dart'; + +class NotificationTypeTile extends ConsumerStatefulWidget { + final NotificationReminderType type; + final VoidCallback setNotificationTypeCallback; + + const NotificationTypeTile( + {Key? key, required this.type, required this.setNotificationTypeCallback}) + : super(key: key); + + @override + _NotificationTypeTileState createState() => _NotificationTypeTileState(); +} + +class _NotificationTypeTileState extends ConsumerState { + late SharedPreferences prefs; + bool isPrefInizialized = false; + + @override + void initState() { + super.initState(); + initPrefs(); + } + + Future initPrefs() async { + prefs = await SharedPreferences.getInstance(); + setState(() { + isPrefInizialized = true; + }); + } + + @override + Widget build(BuildContext context) { + if (!isPrefInizialized) return Container(); + + NotificationReminderType? notificationReminderType = + NotificationReminderType?.values?.firstWhere((e) => + e.toString().split('.').last == + prefs.getString('transaction-reminder-cadence')); + + final typeName = + "${widget.type.name[0].toUpperCase()}${widget.type.name.substring(1).toLowerCase()}"; + + return GestureDetector( + onTap: () { + widget.setNotificationTypeCallback.call(); + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + width: MediaQuery.of(context).size.width, + height: 40, + decoration: BoxDecoration( + border: Border.all( + color: notificationReminderType == widget.type + ? Colors.blue + : Colors.grey, + ), + borderRadius: BorderRadius.circular(4.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text( + typeName, + style: TextStyle( + color: notificationReminderType == widget.type + ? Colors.blue + : Colors.black, + ), + ), + const Spacer(), + notificationReminderType == widget.type + ? const Icon( + Icons.check, + color: Colors.blue, + ) + : Container() + ], + ), + ), + ), + ); + } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index f98bafac..bdfbbefd 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,7 +1,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +enum NotificationReminderType { + none, + daily, + weekly, + monthly, +} + final transactionReminderSwitchProvider = StateProvider((ref) => false); +final transactionReminderCadenceProvider = StateProvider((ref) => NotificationReminderType.none); final transactionRecReminderSwitchProvider = StateProvider((ref) => false); final transactionRecAddedSwitchProvider = StateProvider((ref) => false); @@ -14,6 +22,7 @@ class AsyncSettingsNotifier extends AsyncNotifier { Future _getSettings() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); ref.read(transactionReminderSwitchProvider.notifier).state = prefs.getBool('transaction-reminder') ?? false; + ref.read(transactionReminderCadenceProvider.notifier).state = prefs.getString('transaction-reminder-cadence') as NotificationReminderType; ref.read(transactionRecReminderSwitchProvider.notifier).state = prefs.getBool('transaction-rec-reminder') ?? false; ref.read(transactionRecAddedSwitchProvider.notifier).state = prefs.getBool('transaction-rec-added') ?? false; } @@ -23,8 +32,8 @@ class AsyncSettingsNotifier extends AsyncNotifier { state = await AsyncValue.guard(() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setBool('transaction-reminder', ref.read(transactionReminderSwitchProvider)); - await prefs.setBool( - 'transaction-rec-reminder', ref.read(transactionRecReminderSwitchProvider)); + await prefs.setString('transaction-reminder-cadence', ref.read(transactionReminderCadenceProvider).name); + await prefs.setBool('transaction-rec-reminder', ref.read(transactionRecReminderSwitchProvider)); await prefs.setBool('transaction-rec-added', ref.read(transactionRecAddedSwitchProvider)); return _getSettings(); diff --git a/lib/utils/worker_manager.dart b/lib/utils/worker_manager.dart new file mode 100644 index 00000000..1a557211 --- /dev/null +++ b/lib/utils/worker_manager.dart @@ -0,0 +1,45 @@ +import "package:flutter_local_notifications/flutter_local_notifications.dart"; +import "package:workmanager/workmanager.dart"; + +import "../pages/notifications/notifications_service.dart"; +import "../providers/settings_provider.dart"; + +//tasks +const taskShowNotification = "showNotification"; + +void callbackDispatcher() async { + Workmanager().executeTask((task, inputData) async { + switch (task) { + case taskShowNotification: + await notificationsPlugin.show( + 0, + "Weekly reminder", + "Remember to fill your weekly movements!", + const NotificationDetails( + android: AndroidNotificationDetails( + 'sossoldi#reminder', 'reminder', + importance: Importance.max, priority: Priority.max), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ), + )); + } + return Future.value(true); + }); +} + +void scheduleTransactionReminder(NotificationReminderType type) { + Workmanager().cancelByUniqueName("#reminder").then((value) => { + Workmanager().registerPeriodicTask("sossoldi#reminder", taskShowNotification, frequency: Duration(days: type == NotificationReminderType.daily ? 1 : type == NotificationReminderType.weekly ? 7 : 30)) + }); +} + +void scheduleAlertRecursiveTransaction() { + //TODO + Workmanager().cancelByUniqueName("#alert").then((value) => { + Workmanager().registerPeriodicTask("#alert", taskShowNotification, + frequency: Duration(days: 1)) + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index e049369e..7a3bc2e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,9 @@ dependencies: circular_menu: ^2.0.1 flutter_native_splash: ^2.2.18 shared_preferences: ^2.2.2 + flutter_local_notifications: ^16.3.2 + permission_handler: ^11.2.0 + workmanager: ^0.5.2 dev_dependencies: flutter_test: From 5f02f56b96fbb362a7e589058aeac58052a806e2 Mon Sep 17 00:00:00 2001 From: K-w-e Date: Fri, 9 Feb 2024 11:41:11 +0100 Subject: [PATCH 2/5] Fix entryPoint on worker_manager --- lib/main.dart | 2 +- lib/utils/worker_manager.dart | 3 ++- pubspec.yaml | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4efc3dd4..135e2033 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); requestNotificationPermissions(); initializeNotifications(); - Workmanager().initialize(callbackDispatcher, isInDebugMode: true); + Workmanager().initialize(callbackDispatcher); initializeDateFormatting('it_IT', null).then((_) => runApp(const ProviderScope(child: Launcher()))); } diff --git a/lib/utils/worker_manager.dart b/lib/utils/worker_manager.dart index 1a557211..a15f7869 100644 --- a/lib/utils/worker_manager.dart +++ b/lib/utils/worker_manager.dart @@ -7,6 +7,7 @@ import "../providers/settings_provider.dart"; //tasks const taskShowNotification = "showNotification"; +@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ void callbackDispatcher() async { Workmanager().executeTask((task, inputData) async { switch (task) { @@ -40,6 +41,6 @@ void scheduleAlertRecursiveTransaction() { //TODO Workmanager().cancelByUniqueName("#alert").then((value) => { Workmanager().registerPeriodicTask("#alert", taskShowNotification, - frequency: Duration(days: 1)) + frequency: const Duration(days: 30)) }); } diff --git a/pubspec.yaml b/pubspec.yaml index 7a3bc2e3..159073e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,9 +61,9 @@ dependencies: circular_menu: ^2.0.1 flutter_native_splash: ^2.2.18 shared_preferences: ^2.2.2 - flutter_local_notifications: ^16.3.2 - permission_handler: ^11.2.0 - workmanager: ^0.5.2 + flutter_local_notifications: 16.3.2 + permission_handler: 11.2.0 + workmanager: 0.5.2 dev_dependencies: flutter_test: From c85660d12abf91a746340128e6d9b27ad4ea7818 Mon Sep 17 00:00:00 2001 From: K-w-e Date: Fri, 9 Feb 2024 19:06:13 +0100 Subject: [PATCH 3/5] Last adjustments for showReminder task --- lib/utils/worker_manager.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/utils/worker_manager.dart b/lib/utils/worker_manager.dart index a15f7869..bb0611db 100644 --- a/lib/utils/worker_manager.dart +++ b/lib/utils/worker_manager.dart @@ -5,7 +5,7 @@ import "../pages/notifications/notifications_service.dart"; import "../providers/settings_provider.dart"; //tasks -const taskShowNotification = "showNotification"; +const taskShowNotification = "showReminder"; @pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ void callbackDispatcher() async { @@ -14,8 +14,8 @@ void callbackDispatcher() async { case taskShowNotification: await notificationsPlugin.show( 0, - "Weekly reminder", - "Remember to fill your weekly movements!", + "Remember to log new expenses!", + "Sossoldi is waiting for your latest transactions. Don't lose sight of your financial movements, and input your most recent expenses.", const NotificationDetails( android: AndroidNotificationDetails( 'sossoldi#reminder', 'reminder', @@ -31,16 +31,15 @@ void callbackDispatcher() async { }); } -void scheduleTransactionReminder(NotificationReminderType type) { - Workmanager().cancelByUniqueName("#reminder").then((value) => { - Workmanager().registerPeriodicTask("sossoldi#reminder", taskShowNotification, frequency: Duration(days: type == NotificationReminderType.daily ? 1 : type == NotificationReminderType.weekly ? 7 : 30)) - }); +void scheduleTransactionReminder(NotificationReminderType type) async { + await Workmanager().cancelByUniqueName("sossoldi#reminder"); + await Workmanager().registerPeriodicTask("sossoldi#reminder", taskShowNotification, frequency: Duration(days: type == NotificationReminderType.daily ? 1 : type == NotificationReminderType.weekly ? 7 : 30)); } void scheduleAlertRecursiveTransaction() { //TODO - Workmanager().cancelByUniqueName("#alert").then((value) => { - Workmanager().registerPeriodicTask("#alert", taskShowNotification, + Workmanager().cancelByUniqueName("sossoldi#alert").then((value) => { + Workmanager().registerPeriodicTask("sossoldi#alert", taskShowNotification, frequency: const Duration(days: 30)) }); } From f9acb452507a31d18b553a0c592c7a3a9346b561 Mon Sep 17 00:00:00 2001 From: K-w-e Date: Fri, 9 Feb 2024 21:14:33 +0100 Subject: [PATCH 4/5] Disable reminder channel notifications --- lib/pages/notifications/notifications_settings.dart | 1 + lib/utils/worker_manager.dart | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lib/pages/notifications/notifications_settings.dart b/lib/pages/notifications/notifications_settings.dart index e78d0428..4a5eba6d 100644 --- a/lib/pages/notifications/notifications_settings.dart +++ b/lib/pages/notifications/notifications_settings.dart @@ -93,6 +93,7 @@ class _NotificationsSettingsState extends ConsumerState { .read(transactionReminderSwitchProvider.notifier) .state = value; ref.read(settingsProvider.notifier).updateNotifications(); + toggleTransactionReminder(value); }, ), ], diff --git a/lib/utils/worker_manager.dart b/lib/utils/worker_manager.dart index bb0611db..e5caeda0 100644 --- a/lib/utils/worker_manager.dart +++ b/lib/utils/worker_manager.dart @@ -31,6 +31,12 @@ void callbackDispatcher() async { }); } +void toggleTransactionReminder(bool toActive) async { + if(!toActive){ + await Workmanager().cancelByUniqueName("sossoldi#reminder"); + } +} + void scheduleTransactionReminder(NotificationReminderType type) async { await Workmanager().cancelByUniqueName("sossoldi#reminder"); await Workmanager().registerPeriodicTask("sossoldi#reminder", taskShowNotification, frequency: Duration(days: type == NotificationReminderType.daily ? 1 : type == NotificationReminderType.weekly ? 7 : 30)); From 389eb689f864b1a01486afe2c3b3ad3d51977ec7 Mon Sep 17 00:00:00 2001 From: K-w-e Date: Fri, 22 Mar 2024 19:21:41 +0100 Subject: [PATCH 5/5] Hiding null option --- lib/main.dart | 10 +++++++--- lib/pages/settings_page.dart | 16 ++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 135e2033..a4131834 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -11,9 +13,11 @@ import 'utils/app_theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - requestNotificationPermissions(); - initializeNotifications(); - Workmanager().initialize(callbackDispatcher); + if(Platform.isAndroid){ + requestNotificationPermissions(); + initializeNotifications(); + Workmanager().initialize(callbackDispatcher); + } initializeDateFormatting('it_IT', null).then((_) => runApp(const ProviderScope(child: Launcher()))); } diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 03a60356..fecef047 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -2,6 +2,8 @@ // ignore_for_file: unused_result +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sossoldi/providers/statistics_provider.dart'; @@ -23,7 +25,7 @@ class SettingsPage extends ConsumerStatefulWidget { ConsumerState createState() => _SettingsPageState(); } -var settingsOptions = const [ +var settingsOptions = [ [ Icons.settings, "General Settings", @@ -58,7 +60,7 @@ var settingsOptions = const [ Icons.notifications_active, "Notifications", "Manage your notifications settings", - "/notifications-settings", + Platform.isAndroid ? "/notifications-settings" : null, ], [ Icons.info, @@ -109,14 +111,16 @@ class _SettingsPageState extends ConsumerState { ], ), ), - ListView.separated( + ListView.builder( itemCount: settingsOptions.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, i) { List setting = settingsOptions[i]; - return DefaultCard( + if (setting[3] == null) return Container(); + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: DefaultCard( onTap: () { if (setting[3] != null) { Navigator.of(context).pushNamed(setting[3] as String); @@ -163,7 +167,7 @@ class _SettingsPageState extends ConsumerState { ), ], ), - ); + )); }, ), ],