From f0c2942861e65411de612617c085e7a5c39c448a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sat, 13 Jun 2026 22:32:10 +0700 Subject: [PATCH] feat: implement notification detail view and add support for permission handling and app badges --- .flutter-plugins-dependencies | 2 +- android/app/src/main/AndroidManifest.xml | 1 + lib/main.dart | 41 +++- lib/screens/login_screen.dart | 14 +- lib/screens/loyalty_dashboard.dart | 125 ++++++++++- lib/screens/notification_detail_screen.dart | 131 +++++++++++ lib/screens/notifications_screen.dart | 210 ++++++++++++++---- lib/screens/settings_screen.dart | 35 +++ lib/services/background_service.dart | 86 ++++--- lib/services/notification_service.dart | 109 +++++++++ lib/services/odoo_service.dart | 15 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 56 +++++ pubspec.yaml | 2 + test/widget_test.dart | 25 +-- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 17 files changed, 737 insertions(+), 121 deletions(-) create mode 100644 lib/screens/notification_detail_screen.dart create mode 100644 lib/services/notification_service.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index b1ea07b..9c071df 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_apple-0.9.1+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_android-0.9.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_local_notifications_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-8.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_local_notifications_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"workmanager","dependencies":["workmanager_android","workmanager_apple"]},{"name":"workmanager_android","dependencies":[]},{"name":"workmanager_apple","dependencies":[]}],"date_created":"2026-03-27 08:55:53.380396","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.10/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_apple-0.9.1+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_android-0.9.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_local_notifications_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-8.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_local_notifications_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"permission_handler_html","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_badge_plus","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"workmanager","dependencies":["workmanager_android","workmanager_apple"]},{"name":"workmanager_android","dependencies":[]},{"name":"workmanager_apple","dependencies":[]}],"date_created":"2026-06-13 21:28:54.078112","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9886387..4e203d5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + ; + final session = OdooSession.fromJson(sessionMap); + final service = OdooService(); + service.connect(AppConfig.odooUrl, session: session); + homeWidget = LoyaltyDashboard(partnerId: session.partnerId); + } catch (e) { + homeWidget = const LoginScreen(); + } + } + + runApp(OdooLoyaltyApp(homeWidget: homeWidget)); } class OdooLoyaltyApp extends StatelessWidget { - const OdooLoyaltyApp({super.key}); + final Widget homeWidget; + const OdooLoyaltyApp({super.key, required this.homeWidget}); @override Widget build(BuildContext context) { @@ -33,7 +62,7 @@ class OdooLoyaltyApp extends StatelessWidget { title: 'Mie Mapan Loyalty App', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, - home: const LoginScreen(), + home: homeWidget, ); } } diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 26dfacc..126da8f 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -36,18 +36,8 @@ class _LoginScreenState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.setString('odoo_url', AppConfig.odooUrl); - final sessionJson = json.encode({ - 'id': session.id, - 'user_id': session.userId, - 'partner_id': session.partnerId, - 'user_login': session.userLogin, - 'user_name': session.userName, - 'user_lang': session.userLang, - 'user_tz': session.userTz, - 'is_system': session.isSystem, - 'server_version': session.serverVersion, - }); - await prefs.setString('odoo_session', sessionJson); + // Use the library's own toJson() to ensure keys exactly match fromJson() on restore + await prefs.setString('odoo_session', json.encode(session.toJson())); if (mounted) { Navigator.pushReplacement( diff --git a/lib/screens/loyalty_dashboard.dart b/lib/screens/loyalty_dashboard.dart index 7ab3840..ed4eb69 100644 --- a/lib/screens/loyalty_dashboard.dart +++ b/lib/screens/loyalty_dashboard.dart @@ -1,5 +1,8 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../services/odoo_service.dart'; +import '../services/notification_service.dart'; import '../theme/app_theme.dart'; import 'notifications_screen.dart'; import 'branches_screen.dart'; @@ -16,16 +19,98 @@ class LoyaltyDashboard extends StatefulWidget { class _LoyaltyDashboardState extends State { List _loyaltyCards = []; bool _isLoading = true; + int _unreadNotificationCount = 0; + Timer? _notificationTimer; + + // Shared pref keys + static const _kLastNotified = 'last_device_notified_id'; @override void initState() { super.initState(); _fetchLoyaltyData(); + _fetchNotificationCount(); + _notificationTimer = Timer.periodic(const Duration(seconds: 10), (_) { + _fetchNotificationCount(); + }); + } + + @override + void dispose() { + _notificationTimer?.cancel(); + super.dispose(); + } + + Future _fetchNotificationCount() async { + try { + final client = OdooService().client; + if (client == null) return; + + final response = await client.callRPC( + '/api/loyalty/fetch_notifications', + 'call', + {'last_id': 0}, + ); + + if (response != null && response['status'] == 'success') { + final List notifs = response['data'] ?? []; + final prefs = await SharedPreferences.getInstance(); + final lastNotifiedId = prefs.getInt(_kLastNotified) ?? 0; + + // Check read list + final readIds = prefs.getStringList('read_notification_ids'); + int unreadCount = 0; + + if (readIds == null) { + final initialRead = notifs.map((n) => (n['id'] as int? ?? 0).toString()).toList(); + await prefs.setStringList('read_notification_ids', initialRead); + unreadCount = 0; + } else { + unreadCount = notifs + .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString())) + .length; + } + + int highestNewId = lastNotifiedId; + final List toNotify = []; + + for (var notif in notifs) { + final id = notif['id'] as int? ?? 0; + if (id > lastNotifiedId) { + toNotify.add(notif); + if (id > highestNewId) highestNewId = id; + } + } + + // Show device tray notifications for any not yet shown + if (toNotify.isNotEmpty) { + final notifService = NotificationService(); + for (final notif in toNotify) { + await notifService.showNotification( + id: notif['id'] as int, + title: notif['title'] ?? 'Mie Mapan', + body: notif['body'] ?? '', + ); + } + await prefs.setInt(_kLastNotified, highestNewId); + } + + // Always update system badge to match the unread count + await NotificationService().setBadge(unreadCount); + + if (mounted) { + setState(() => _unreadNotificationCount = unreadCount); + } + } + } catch (e) { + // ignore + } } Future _fetchLoyaltyData() async { try { final cards = await OdooService().getLoyaltyCards(widget.partnerId); + await _fetchNotificationCount(); setState(() { _loyaltyCards = cards; _isLoading = false; @@ -38,6 +123,7 @@ class _LoyaltyDashboardState extends State { } } + @override Widget build(BuildContext context) { return Scaffold( @@ -48,9 +134,42 @@ class _LoyaltyDashboardState extends State { icon: const Icon(Icons.storefront), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BranchesScreen())), ), - IconButton( - icon: const Icon(Icons.notifications), - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen())), + Stack( + clipBehavior: Clip.none, + children: [ + IconButton( + icon: const Icon(Icons.notifications), + onPressed: () async { + await Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen())); + _fetchNotificationCount(); + }, + ), + if (_unreadNotificationCount > 0) + Positioned( + right: 8, + top: 8, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Text( + '$_unreadNotificationCount', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], ), IconButton( icon: const Icon(Icons.settings), diff --git a/lib/screens/notification_detail_screen.dart b/lib/screens/notification_detail_screen.dart new file mode 100644 index 0000000..ead02aa --- /dev/null +++ b/lib/screens/notification_detail_screen.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import '../theme/app_theme.dart'; +import '../services/odoo_service.dart'; + +class NotificationDetailScreen extends StatelessWidget { + final dynamic notif; + + const NotificationDetailScreen({super.key, required this.notif}); + + @override + Widget build(BuildContext context) { + final title = notif['title'] as String? ?? 'Notice'; + final body = notif['body'] as String? ?? ''; + + return Scaffold( + appBar: AppBar( + title: const Text('Notification Detail'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header icon block + Container( + padding: const EdgeInsets.symmetric(vertical: 32), + color: AppTheme.surfaceContainerLow, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.primary.withValues(alpha: 0.15), + shape: BoxShape.rectangle, + ), + child: const Icon( + Icons.campaign_outlined, + color: AppTheme.secondary, + size: 40, + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + title, + textAlign: TextAlign.center, + style: + Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.onSurface, + fontSize: 20, + ), + ), + ), + ], + ), + ), + + if (notif['has_image'] == true) ...[ + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + OdooService().notificationImageUrl(notif['id'] as int), + headers: { + 'Cookie': OdooService().sessionCookie, + }, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 200, + color: AppTheme.surfaceContainerLow, + child: const Center(child: CircularProgressIndicator()), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 150, + color: AppTheme.surfaceContainerLow, + child: const Center( + child: Icon(Icons.broken_image_outlined, size: 48, color: AppTheme.onSurfaceVariant), + ), + ); + }, + ), + ), + ], + + const SizedBox(height: 24), + + // Body label + Text( + 'Message', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: AppTheme.onSurfaceVariant, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + + // Divider line (editorial style) + const Divider( + color: AppTheme.surfaceContainerHighest, + thickness: 2, + height: 2, + ), + const SizedBox(height: 16), + + // Body text + body.isEmpty + ? Text( + 'No additional details.', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.onSurfaceVariant, + fontStyle: FontStyle.italic, + ), + ) + : Text( + body, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.onSurface, + height: 1.7, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index 22b7fca..a51f044 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:odoo_rpc/odoo_rpc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../services/odoo_service.dart'; +import '../services/notification_service.dart'; import '../theme/app_theme.dart'; +import 'notification_detail_screen.dart'; class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); @@ -12,6 +15,7 @@ class NotificationsScreen extends StatefulWidget { class _NotificationsScreenState extends State { List _notifications = []; + List _readIds = []; bool _isLoading = true; @override @@ -23,28 +27,35 @@ class _NotificationsScreenState extends State { Future _fetchNotifications() async { try { final client = OdooService().client; - if (client == null) throw Exception("Not connected"); - + if (client == null) throw Exception('Not connected'); + final response = await client.callRPC( - '/api/loyalty/fetch_notifications', - 'call', - {'last_id': 0} + '/api/loyalty/fetch_notifications', + 'call', + {'last_id': 0}, ); - + if (response != null && response['status'] == 'success') { + final List fetched = response['data'] ?? []; + final prefs = await SharedPreferences.getInstance(); + final readIds = prefs.getStringList('read_notification_ids') ?? []; + if (mounted) { setState(() { - _notifications = response['data']; + _notifications = fetched; + _readIds = readIds; _isLoading = false; }); } } else { - throw Exception("Invalid response from server"); + throw Exception('Invalid response from server'); } } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading notifications: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading notifications: $e')), + ); } } } @@ -56,43 +67,154 @@ class _NotificationsScreenState extends State { body: _isLoading ? const Center(child: CircularProgressIndicator()) : _notifications.isEmpty - ? const Center(child: Text('No new promos.', style: TextStyle(fontSize: 16))) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - itemCount: _notifications.length, - itemBuilder: (context, index) { - final notif = _notifications[index]; - return Container( - margin: const EdgeInsets.only(bottom: 16), - decoration: const BoxDecoration( - color: AppTheme.surfaceContainerLow, - borderRadius: BorderRadius.zero, + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.notifications_none, + size: 64, + color: AppTheme.onSurfaceVariant.withValues(alpha: 0.4), ), - child: ListTile( - contentPadding: const EdgeInsets.all(16), - leading: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.primaryContainer.withOpacity(0.1), - shape: BoxShape.rectangle, - ), - child: const Icon(Icons.star, color: AppTheme.primary), - ), - title: Text( - notif['title'] ?? 'Notice', - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - notif['body'] ?? '', - style: Theme.of(context).textTheme.bodyMedium, - ), - ), + const SizedBox(height: 16), + Text( + 'No notifications yet.', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.onSurfaceVariant, + ), ), - ); - }, + ], + ), + ) + : RefreshIndicator( + onRefresh: _fetchNotifications, + child: ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 20), + itemCount: _notifications.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final notif = _notifications[index]; + final isUnread = !_readIds.contains((notif['id'] as int? ?? 0).toString()); + return _NotificationCard( + notif: notif, + isUnread: isUnread, + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + final readIds = prefs.getStringList('read_notification_ids') ?? []; + final notifIdStr = (notif['id'] as int? ?? 0).toString(); + if (!readIds.contains(notifIdStr)) { + readIds.add(notifIdStr); + await prefs.setStringList('read_notification_ids', readIds); + + // Recalculate and update system badge count + final unreadCount = _notifications + .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString())) + .length; + await NotificationService().setBadge(unreadCount); + } + + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + NotificationDetailScreen(notif: notif), + ), + ); + _fetchNotifications(); + }, + ); + }, + ), ), ); } } + +class _NotificationCard extends StatelessWidget { + final dynamic notif; + final bool isUnread; + final VoidCallback onTap; + + const _NotificationCard({ + required this.notif, + required this.isUnread, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final title = notif['title'] as String? ?? 'Notice'; + + return Material( + color: AppTheme.surfaceContainerLow, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + child: Row( + children: [ + notif['has_image'] == true && notif['image_128'] != null + ? Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(4), + image: DecorationImage( + image: MemoryImage( + base64Decode(notif['image_128'] as String), + ), + fit: BoxFit.cover, + ), + ), + ) + : Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppTheme.primary.withValues(alpha: 0.15), + shape: BoxShape.rectangle, + ), + child: const Icon( + Icons.campaign_outlined, + color: AppTheme.secondary, + size: 22, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: AppTheme.onSurface, + fontWeight: isUnread ? FontWeight.bold : FontWeight.normal, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + if (isUnread) ...[ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + ], + const Icon( + Icons.chevron_right, + color: AppTheme.onSurfaceVariant, + size: 20, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d4949aa..f862073 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/odoo_service.dart'; +import '../services/notification_service.dart'; import '../theme/app_theme.dart'; import 'login_screen.dart'; @@ -177,6 +178,21 @@ class _SettingsScreenState extends State { ); } + void _logout() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('odoo_session'); + await prefs.remove('last_seen_notification_id'); + await prefs.remove('last_device_notified_id'); + await prefs.remove('read_notification_ids'); + await NotificationService().clearBadge(); + if (mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginScreen()), + (route) => false, + ); + } + } + @override void dispose() { _phraseController.dispose(); @@ -234,6 +250,25 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 40), + ElevatedButton( + onPressed: _logout, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: AppTheme.onSurface, + side: const BorderSide(color: AppTheme.surfaceContainer, width: 2), + ), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'Log Out', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + const SizedBox(height: 16), ElevatedButton( onPressed: _showDeleteConfirmationDialog, style: ElevatedButton.styleFrom( diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index e5e8922..1b8b8fb 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,8 +1,12 @@ import 'dart:convert'; import 'package:workmanager/workmanager.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:odoo_rpc/odoo_rpc.dart'; +import 'notification_service.dart'; + +// NOTE: This key tracks what IDs have been shown as device tray notifications. +// It is separate from 'last_seen_notification_id' (which tracks what the user READ in-app). +const String _kLastDeviceNotifiedId = 'last_device_notified_id'; @pragma('vm:entry-point') void callbackDispatcher() { @@ -11,52 +15,70 @@ void callbackDispatcher() { final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('odoo_url'); final sessionStr = prefs.getString('odoo_session'); - final lastNotificationId = prefs.getInt('last_notification_id') ?? 0; if (url == null || sessionStr == null) { - return Future.value(true); // Cannot fetch if not logged in + return Future.value(true); // Not logged in, nothing to do } + final lastDeviceNotifiedId = prefs.getInt(_kLastDeviceNotifiedId) ?? 0; + final sessionArgs = json.decode(sessionStr); - final session = OdooSession.fromJson(sessionArgs); + final session = OdooSession.fromJson( + Map.from(sessionArgs as Map)); final client = OdooClient(url, sessionId: session); - + final response = await client.callRPC( - '/api/loyalty/fetch_notifications', - 'call', - {'last_id': lastNotificationId} + '/api/loyalty/fetch_notifications', + 'call', + {'last_id': 0}, ); - if (response != null && response['status'] == 'success') { - final notifications = response['data'] as List; - if (notifications.isNotEmpty) { - int highestId = lastNotificationId; - - final flnp = FlutterLocalNotificationsPlugin(); - const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); - const initSettings = InitializationSettings(android: androidSettings); - await flnp.initialize(settings: initSettings); + client.close(); - for (final notif in notifications) { - final int notifId = notif['id']; + if (response != null && response['status'] == 'success') { + final List notifications = + List.from(response['data'] ?? []); + + // Filter to only truly new ones not yet shown on device tray + final newNotifs = notifications + .where((n) => (n['id'] as int? ?? 0) > lastDeviceNotifiedId) + .toList(); + + final notifService = NotificationService(); + await notifService.initialize(); + + if (newNotifs.isNotEmpty) { + int highestId = lastDeviceNotifiedId; + for (final notif in newNotifs) { + final int notifId = notif['id'] as int? ?? 0; if (notifId > highestId) highestId = notifId; - - const androidConfig = AndroidNotificationDetails( - 'loyalty_channel', 'Promos', - importance: Importance.max, priority: Priority.high - ); - await flnp.show( - id: notifId, - title: notif['title'], - body: notif['body'], - notificationDetails: const NotificationDetails(android: androidConfig) + + await notifService.showNotification( + id: notifId, + title: notif['title'] as String? ?? 'Mie Mapan', + body: notif['body'] as String? ?? '', ); } - await prefs.setInt('last_notification_id', highestId); + + await prefs.setInt(_kLastDeviceNotifiedId, highestId); + } + + // Always compute badge count based on read_notification_ids + final readIds = prefs.getStringList('read_notification_ids'); + if (readIds == null) { + // Initialize read list with all currently fetched notifications on first install/run + final initialRead = notifications.map((n) => (n['id'] as int? ?? 0).toString()).toList(); + await prefs.setStringList('read_notification_ids', initialRead); + await notifService.setBadge(0); + } else { + final unreadCount = notifications + .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString())) + .length; + await notifService.setBadge(unreadCount); } } - } catch (e) { - print('Background Fetch Error: $e'); + } catch (_) { + // Silently swallow — background isolate must never crash } return Future.value(true); }); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 0000000..96b150d --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,109 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:app_badge_plus/app_badge_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// Central service for all local notification and badge operations. +/// Call [initialize] once in main(), then use [showNotification] and [setBadge]. +class NotificationService { + static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; + NotificationService._internal(); + + static const String _channelId = 'mapan_loyalty_channel'; + static const String _channelName = 'Mie Mapan Promos'; + static const String _channelDescription = + 'Notifications for new promos and loyalty rewards from Mie Mapan.'; + + final FlutterLocalNotificationsPlugin _flnp = FlutterLocalNotificationsPlugin(); + + bool _initialized = false; + + Future initialize() async { + if (_initialized) return; + + const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); + const darwinInit = DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ); + const initSettings = InitializationSettings( + android: androidInit, + iOS: darwinInit, + ); + + await _flnp.initialize( + settings: initSettings, + ); + await _createNotificationChannel(); + _initialized = true; + } + + Future _createNotificationChannel() async { + const channel = AndroidNotificationChannel( + _channelId, + _channelName, + description: _channelDescription, + importance: Importance.max, + ); + await _flnp + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + } + + /// Request POST_NOTIFICATIONS permission on Android 13+. + Future requestPermission() async { + if (await Permission.notification.isDenied) { + await Permission.notification.request(); + } + } + + /// Show a single notification in the device tray. + Future showNotification({ + required int id, + required String title, + required String body, + }) async { + await initialize(); + const androidDetails = AndroidNotificationDetails( + _channelId, + _channelName, + channelDescription: _channelDescription, + importance: Importance.max, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + ); + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + await _flnp.show( + id: id, + title: title, + body: body, + notificationDetails: details, + ); + } + + /// Set the badge count on the app icon. + Future setBadge(int count) async { + try { + final supported = await AppBadgePlus.isSupported(); + if (supported) { + await AppBadgePlus.updateBadge(count); + } + } catch (e) { + debugPrint('Badge update error: $e'); + } + } + + /// Clear the badge count (set to 0). + Future clearBadge() => setBadge(0); +} diff --git a/lib/services/odoo_service.dart b/lib/services/odoo_service.dart index 43c0c38..94ed2b3 100644 --- a/lib/services/odoo_service.dart +++ b/lib/services/odoo_service.dart @@ -8,10 +8,21 @@ class OdooService { OdooClient? client; - void connect(String url) { - client = OdooClient(url); + void connect(String url, {OdooSession? session}) { + client = OdooClient(url, sessionId: session); } + /// Returns the session cookie header value for authenticated image loading. + /// Usage: Image.network(url, headers: {'Cookie': OdooService().sessionCookie}) + String get sessionCookie { + final sessionId = client?.sessionId?.id ?? ''; + return 'session_id=$sessionId'; + } + + /// Returns the URL for the full notification image. + String notificationImageUrl(int notifId) => + '${AppConfig.odooUrl}/web/image/mapan.app.notification/$notifId/image'; + Future login(String db, String username, String password) async { if (client == null) throw Exception("Connect to Odoo first"); return await client!.authenticate(db, username, password); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6f01d65..3fc006e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import app_badge_plus import flutter_local_notifications import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppBadgePlusPlugin.register(with: registry.registrar(forPlugin: "AppBadgePlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 93c0883..bd7dd1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + app_badge_plus: + dependency: "direct main" + description: + name: app_badge_plus + sha256: "60b24ecbcb792f5b185a837b066ee436ef9f6435f2a3006eb314d0c6a80f1d75" + url: "https://pub.dev" + source: hosted + version: "1.3.1" archive: dependency: transitive description: @@ -400,6 +408,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" + url: "https://pub.dev" + source: hosted + version: "9.4.10" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d8eb043..3eb0b5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: shared_preferences: ^2.5.4 google_fonts: ^6.2.1 url_launcher: ^6.3.2 + app_badge_plus: ^1.3.1 + permission_handler: ^12.0.3 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 97876e0..ea2e2b3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,29 +1,12 @@ // This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:odoo_loyalty_app/main.dart'; +import 'package:odoo_loyalty_app/screens/login_screen.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - await tester.pumpWidget(const OdooLoyaltyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + testWidgets('App renders without crash', (WidgetTester tester) async { + await tester.pumpWidget(const OdooLoyaltyApp(homeWidget: LoginScreen())); + expect(find.byType(OdooLoyaltyApp), findsOneWidget); }); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4f78848..a0d0bbe 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 83dbdeb..ef51301 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows url_launcher_windows )