diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 486f116eeb8dc..ef435c0efdcc7 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -214,5 +214,11 @@ "version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" -} \ No newline at end of file + "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", + "notification_permission_dialog_cancel": "Cancel", + "notification_permission_dialog_settings": "Settings", + "notification_permission_list_tile_title": "Notification Permission", + "notification_permission_list_tile_content": "Grant permission to enable notifications.", + "notification_permission_list_tile_enable_button": "Enable Notifications" +} diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index fbc07d8797ae2..e9e52b5d071ea 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -37,5 +37,66 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + # Permission macros for the permission_handler (https://pub.dev/packages/permission_handler) + # Start of the permission_handler configuration + # Remove the # character in front of the permission you do want to use. + target.build_configurations.each do |config| + + # You can enable the permissions needed here. For example to enable camera + # permission, just remove the `#` character in front so it looks like this: + # + # ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1' + # + # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + # 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + # 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.photos + # 'PERMISSION_PHOTOS=1', + + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + # 'PERMISSION_LOCATION=1', + + ## dart: PermissionGroup.notification + 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + # 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + # 'PERMISSION_CRITICAL_ALERTS=1' + ] + + end + # End of the permission_handler configuration end -end \ No newline at end of file +end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index fb08564767410..53abe51c22ccf 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -26,6 +26,8 @@ PODS: - FlutterMacOS - path_provider_ios (0.0.1): - Flutter + - permission_handler_apple (9.0.4): + - Flutter - photo_manager (2.0.0): - Flutter - FlutterMacOS @@ -58,6 +60,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) @@ -95,6 +98,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" share_plus: @@ -123,6 +128,7 @@ SPEC CHECKSUMS: package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 @@ -133,6 +139,6 @@ SPEC CHECKSUMS: video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: c798208781ca5116c4a3d5927d689946791f0189 +PODFILE CHECKSUM: 4a7e0475ea85ab7bf89955bc4c7ea9d18b54dfd8 COCOAPODS: 1.11.3 diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index c9331a780b8a1..ca3cf4564f3fe 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -1,4 +1,5 @@ import UIKit +import shared_preferences_foundation import Flutter import BackgroundTasks import path_provider_ios @@ -25,6 +26,10 @@ import photo_manager if !registry.hasPlugin("org.cocoapods.photo-manager") { PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) } + + if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) + } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift index 10f1bb24a1add..e391f5187e06d 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift @@ -25,6 +25,7 @@ class BackgroundSyncWorker { name: "BackgroundImmich" ) + let notificationId = "com.alextran.immich/backgroundNotifications" // The background message passing channel var channel: FlutterMethodChannel? @@ -67,15 +68,15 @@ class BackgroundSyncWorker { }) break case "updateNotification": - // TODO: implement update notification - result(true) + let handled = self.handleNotification(call) + result(handled) break case "showError": - // TODO: implement show error - result(true) + let handled = self.handleError(call) + result(handled) break case "clearErrorNotifications": - // TODO: implement clear error notifications + self.handleClearErrorNotifications() result(true) break case "hasContentChanged": @@ -184,5 +185,87 @@ class BackgroundSyncWorker { channel = nil completionHandler(fetchResult) } + + private func handleNotification(_ call: FlutterMethodCall) -> Bool { + + // Parse the arguments as an array list + guard let args = call.arguments as? Array else { + print("Failed to parse \(call.arguments) as array") + return false; + } + + // Requires 7 arguments passed or else fail + guard args.count == 7 else { + print("Needs 7 arguments, but was only passed \(args.count)") + return false + } + + // Parse the arguments to send the notification update + let title = args[0] as? String + let content = args[1] as? String + let progress = args[2] as? Int + let maximum = args[3] as? Int + let indeterminate = args[4] as? Bool + let isDetail = args[5] as? Bool + let onlyIfForeground = args[6] as? Bool + + // Build the notification + let notificationContent = UNMutableNotificationContent() + notificationContent.body = content ?? "Uploading..." + notificationContent.title = title ?? "Immich" + + // Add it to the Notification center + let notification = UNNotificationRequest( + identifier: notificationId, + content: notificationContent, + trigger: nil + ) + let center = UNUserNotificationCenter.current() + center.add(notification) { (error: Error?) in + if let theError = error { + print("Error showing notifications: \(theError)") + } + } + + return true + } + + private func handleError(_ call: FlutterMethodCall) -> Bool { + // Parse the arguments as an array list + guard let args = call.arguments as? Array else { + return false; + } + + // Requires 7 arguments passed or else fail + guard args.count == 3 else { + return false + } + + let title = args[0] as? String + let content = args[1] as? String + let individualTag = args[2] as? String + + // Build the notification + let notificationContent = UNMutableNotificationContent() + notificationContent.body = content ?? "Error running the backup job." + notificationContent.title = title ?? "Immich" + + // Add it to the Notification center + let notification = UNNotificationRequest( + identifier: notificationId, + content: notificationContent, + trigger: nil + ) + let center = UNUserNotificationCenter.current() + center.add(notification) + + return true + } + + private func handleClearErrorNotifications() { + let center = UNUserNotificationCenter.current() + center.removeDeliveredNotifications(withIdentifiers: [notificationId]) + center.removePendingNotificationRequests(withIdentifiers: [notificationId]) + } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index f85189358fac1..5d238e923dcb9 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/modules/settings/providers/permission.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; @@ -126,6 +127,9 @@ class ImmichAppState extends ConsumerState ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); + ref.watch(notificationPermissionProvider.notifier) + .getNotificationPermission(); + break; case AppLifecycleState.inactive: diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 7009414e6cfea..17e9e42c315c1 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -314,10 +314,9 @@ class BackgroundService { return false; } - // Notifications aren't enabled in iOS yet, and this line - // below crashes the iOS background service - if (Platform.isAndroid) { - await loadTranslations(); + final translationsOk = await loadTranslations(); + if (!translationsOk) { + debugPrint("[_callHandler] could not load translations"); } final bool ok = await _onAssetsChanged(); diff --git a/mobile/lib/modules/settings/providers/permission.provider.dart b/mobile/lib/modules/settings/providers/permission.provider.dart new file mode 100644 index 0000000000000..96a9f97966220 --- /dev/null +++ b/mobile/lib/modules/settings/providers/permission.provider.dart @@ -0,0 +1,44 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class NotificationPermissionNotifier extends StateNotifier { + NotificationPermissionNotifier() : + super(Platform.isAndroid + ? PermissionStatus.granted + : PermissionStatus.restricted, + ) { + // Sets the initial state + getNotificationPermission().then((p) => state = p); + } + + /// Requests the notification permission + /// Note: In Android, this is always granted + Future requestNotificationPermission() async { + final permission = await Permission.notification.request(); + state = permission; + return permission; + } + + /// Whether the user has the permission or not + /// Note: In Android, this is always true + Future hasNotificationPermission() { + return Permission.notification.isGranted; + } + + Future getNotificationPermission() async { + final status = await Permission.notification.status; + state = status; + return status; + } + + /// Either the permission was granted already or else ask for the permission + Future hasOrAskForNotificationPermission() { + return requestNotificationPermission().then((p) => p.isGranted); + } + +} +final notificationPermissionProvider + = StateNotifierProvider + ((ref) => NotificationPermissionNotifier()); diff --git a/mobile/lib/modules/settings/services/permission.service.dart b/mobile/lib/modules/settings/services/permission.service.dart new file mode 100644 index 0000000000000..e2e8309af1d57 --- /dev/null +++ b/mobile/lib/modules/settings/services/permission.service.dart @@ -0,0 +1,21 @@ +import 'package:permission_handler/permission_handler.dart'; + +/// This class is for requesting permissions in the app +class PermissionService { + /// Requests the notification permission + /// Note: In Android, this is always granted + Future requestNotificationPermission() { + return Permission.notification.request(); + } + + /// Whether the user has the permission or not + /// Note: In Android, this is always true + Future hasNotificationPermission() { + return Permission.notification.isGranted; + } + + /// Either the permission was granted already or else ask for the permission + Future hasOrAskForNotificationPermission() { + return requestNotificationPermission().then((p) => p.isGranted); + } +} diff --git a/mobile/lib/modules/settings/ui/common.dart b/mobile/lib/modules/settings/ui/common.dart deleted file mode 100644 index 13e1d53e2026d..0000000000000 --- a/mobile/lib/modules/settings/ui/common.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; - -SwitchListTile buildSwitchListTile( - BuildContext context, - AppSettingsService appSettingService, - ValueNotifier valueNotifier, - AppSettingsEnum settingsEnum, { - required String title, - String? subtitle, -}) { - return SwitchListTile.adaptive( - key: Key(settingsEnum.name), - value: valueNotifier.value, - onChanged: (value) { - valueNotifier.value = value; - appSettingService.setSetting(settingsEnum, value); - }, - activeColor: Theme.of(context).primaryColor, - dense: true, - title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: subtitle != null ? Text(subtitle) : null, - ); -} diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart index 88a14d5ad318e..123c7cd00d52c 100644 --- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/modules/settings/ui/common.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; class ImageViewerQualitySetting extends HookConsumerWidget { const ImageViewerQualitySetting({ @@ -44,19 +44,17 @@ class ImageViewerQualitySetting extends HookConsumerWidget { title: const Text('setting_image_viewer_help').tr(), dense: true, ), - buildSwitchListTile( - context, - settings, - isPreview, - AppSettingsEnum.loadPreview, + SettingsSwitchListTile( + appSettingService: settings, + valueNotifier: isPreview, + settingsEnum: AppSettingsEnum.loadPreview, title: "setting_image_viewer_preview_title".tr(), subtitle: "setting_image_viewer_preview_subtitle".tr(), ), - buildSwitchListTile( - context, - settings, - isOriginal, - AppSettingsEnum.loadOriginal, + SettingsSwitchListTile( + appSettingService: settings, + valueNotifier: isOriginal, + settingsEnum: AppSettingsEnum.loadOriginal, title: "setting_image_viewer_original_title".tr(), subtitle: "setting_image_viewer_original_subtitle".tr(), ), diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart index ed996db35b8ea..1028d4e6a071b 100644 --- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart +++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/providers/permission.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/modules/settings/ui/common.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:permission_handler/permission_handler.dart'; class NotificationSetting extends HookConsumerWidget { const NotificationSetting({ @@ -14,12 +16,14 @@ class NotificationSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appSettingService = ref.watch(appSettingsServiceProvider); + final permissionService = ref.watch(notificationPermissionProvider); final sliderValue = useState(0.0); final totalProgressValue = useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue); final singleProgressValue = useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue); + final hasPermission = permissionService == PermissionStatus.granted; useEffect( () { @@ -35,6 +39,30 @@ class NotificationSetting extends HookConsumerWidget { [], ); + // When permissions are permanently denied, you need to go to settings to + // allow them + showPermissionsDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: const Text('notification_permission_dialog_content').tr(), + actions: [ + TextButton( + child: const Text('notification_permission_dialog_cancel').tr(), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: const Text('notification_permission_dialog_settings').tr(), + onPressed: () { + Navigator.of(context).pop(); + openAppSettings(); + }, + ), + ], + ), + ); + } + final String formattedValue = _formatSliderValue(sliderValue.value); return ExpansionTile( textColor: Theme.of(context).primaryColor, @@ -51,23 +79,49 @@ class NotificationSetting extends HookConsumerWidget { ), ).tr(), children: [ - buildSwitchListTile( - context, - appSettingService, - totalProgressValue, - AppSettingsEnum.backgroundBackupTotalProgress, + if (!hasPermission) + ListTile( + leading: const Icon(Icons.notifications_outlined), + title: const Text('notification_permission_list_tile_title').tr(), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('notification_permission_list_tile_content').tr(), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () + => ref.watch(notificationPermissionProvider.notifier) + .requestNotificationPermission().then((permission) { + if (permission == PermissionStatus.permanentlyDenied) { + showPermissionsDialog(); + } + }), + child: + const Text('notification_permission_list_tile_enable_button') + .tr(), + ), + ], + ), + isThreeLine: true, + ), + SettingsSwitchListTile( + enabled: hasPermission, + appSettingService: appSettingService, + valueNotifier: totalProgressValue, + settingsEnum: AppSettingsEnum.backgroundBackupTotalProgress, title: 'setting_notifications_total_progress_title'.tr(), subtitle: 'setting_notifications_total_progress_subtitle'.tr(), ), - buildSwitchListTile( - context, - appSettingService, - singleProgressValue, - AppSettingsEnum.backgroundBackupSingleProgress, + SettingsSwitchListTile( + enabled: hasPermission, + appSettingService: appSettingService, + valueNotifier: singleProgressValue, + settingsEnum: AppSettingsEnum.backgroundBackupSingleProgress, title: 'setting_notifications_single_progress_title'.tr(), subtitle: 'setting_notifications_single_progress_subtitle'.tr(), ), ListTile( + enabled: hasPermission, isThreeLine: false, dense: true, title: const Text( @@ -76,7 +130,7 @@ class NotificationSetting extends HookConsumerWidget { ).tr(args: [formattedValue]), subtitle: Slider( value: sliderValue.value, - onChanged: (double v) => sliderValue.value = v, + onChanged: !hasPermission ? null : (double v) => sliderValue.value = v, onChangeEnd: (double v) => appSettingService.setSetting( AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt(), diff --git a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart new file mode 100644 index 0000000000000..fce440732700f --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; + +class SettingsSwitchListTile extends StatelessWidget { + final AppSettingsService appSettingService; + final ValueNotifier valueNotifier; + final AppSettingsEnum settingsEnum; + final String title; + final bool enabled; + final String? subtitle; + + SettingsSwitchListTile({ + required this.appSettingService, + required this.valueNotifier, + required this.settingsEnum, + required this.title, + this.subtitle, + this.enabled = true, + }) : super(key: Key(settingsEnum.name)); + + @override + Widget build(BuildContext context) { + return SwitchListTile.adaptive( + value: valueNotifier.value, + onChanged: !enabled ? null : (value) { + valueNotifier.value = value; + appSettingService.setSetting(settingsEnum, value); + }, + activeColor: Theme + .of(context) + .primaryColor, + dense: true, + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: subtitle != null ? Text(subtitle!) : null, + ); + } +} diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index 8422e201abcfc..6a207f84e9e25 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -41,7 +39,7 @@ class SettingsPage extends HookConsumerWidget { const ImageViewerQualitySetting(), const ThemeSetting(), const AssetListSettings(), - if (Platform.isAndroid) const NotificationSetting(), + const NotificationSetting(), //const ExperimentalSettings(), ], ).toList(), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 33de5b74db5ae..fb7b2382354c1 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -891,6 +891,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + url: "https://pub.dev" + source: hosted + version: "9.0.7" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6f068ee891931..05ef40de26165 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: easy_image_viewer: ^1.2.0 isar: *isar_version isar_flutter_libs: *isar_version # contains Isar Core + permission_handler: ^10.2.0 openapi: path: openapi