diff --git a/lib/main.dart b/lib/main.dart index 204bb672..a4131834 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,24 @@ +import 'dart:io'; + 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(); + if(Platform.isAndroid){ + requestNotificationPermissions(); + initializeNotifications(); + Workmanager().initialize(callbackDispatcher); + } + 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..4a5eba6d 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,41 @@ 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(); + toggleTransactionReminder(value); }, ), ], ), ), + 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 +146,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 +159,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 +175,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 +194,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/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 { ), ], ), - ); + )); }, ), ], 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..e5caeda0 --- /dev/null +++ b/lib/utils/worker_manager.dart @@ -0,0 +1,51 @@ +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 = "showReminder"; + +@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) { + case taskShowNotification: + await notificationsPlugin.show( + 0, + "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', + importance: Importance.max, priority: Priority.max), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ), + )); + } + return Future.value(true); + }); +} + +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)); +} + +void scheduleAlertRecursiveTransaction() { + //TODO + Workmanager().cancelByUniqueName("sossoldi#alert").then((value) => { + Workmanager().registerPeriodicTask("sossoldi#alert", taskShowNotification, + frequency: const Duration(days: 30)) + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index e049369e..159073e4 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: