import 'dart:async'; import 'dart:developer'; import 'package:app_settings/app_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../session/session_manager.dart'; import '../state/app/modules/app_modules.dart'; import '../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart'; import '../storage/notification_settings.dart'; import '../widget/confirm_dialog.dart'; import 'push_registration.dart'; const _talkExplanation = 'Damit du keine neuen Nachrichten im Talk verpasst, fragen wir dich ' 'gleich nach der Erlaubnis für Benachrichtigungen. Bitte akzeptiere ' 'sie, um Push-Nachrichten zu erhalten.'; const _talkDeclinedNote = 'Ohne die Berechtigung erhältst du keine Benachrichtigungen bei neuen ' 'Talk-Nachrichten. Du kannst sie jederzeit in den Systemeinstellungen ' 'deines Geräts nachträglich aktivieren.'; const _parentLetterExplanation = 'Damit du keine Elternbriefe der Schule verpasst, fragen wir dich gleich ' 'nach der Erlaubnis für Benachrichtigungen. Bitte akzeptiere sie, um ' 'Push-Nachrichten zu erhalten.'; const _parentLetterDeclinedNote = 'Ohne die Berechtigung erhältst du keine Benachrichtigungen bei neuen ' 'Elternbriefen. Du kannst sie jederzeit in den Systemeinstellungen deines ' 'Geräts nachträglich aktivieren.'; /// The occasions with a guided notification-permission flow. Each runs at /// most once per install, guarded by its own flag in [NotificationSettings]. enum _PermissionPrompt { talkVisit(_talkExplanation, _talkDeclinedNote), guardianLogin( _parentLetterExplanation, _parentLetterDeclinedNote, module: Modules.parentLetters, spentOnDisplay: true, ), parentLettersVisit( _parentLetterExplanation, _parentLetterDeclinedNote, module: Modules.parentLetters, ); const _PermissionPrompt( this.explanation, this.declinedNote, { this.module, this.spentOnDisplay = false, }); final String explanation; final String declinedNote; /// Only sessions that have this module are asked. final Modules? module; /// Spends the one-shot as soon as the explanation is displayed, so a /// dismissed dialog does not come back on every app start. final bool spentOnDisplay; bool wasShown(NotificationSettings settings) => switch (this) { talkVisit => settings.talkPermissionPromptShown, guardianLogin => settings.guardianLoginPromptShown, parentLettersVisit => settings.parentLettersPromptShown, }; void markShown(NotificationSettings settings) { switch (this) { case talkVisit: settings.talkPermissionPromptShown = true; case guardianLogin: settings.guardianLoginPromptShown = true; case parentLettersVisit: settings.parentLettersPromptShown = true; } } } /// Shows the one-time notification-permission flow on the first Talk visit. /// /// The OS prompt is deliberately kept out of the cold-start path (younger users /// decline it reflexively before ever seeing why they'd want it). Instead, the /// first time Talk is opened we explain the request, then trigger the OS prompt, /// and — if declined — offer a shortcut to the system settings. /// /// Runs at most once per install (guarded by `talkPermissionPromptShown`). Future maybePromptTalkNotifications(BuildContext context) => _maybePrompt(context, _PermissionPrompt.talkVisit); /// Accounts that receive parent letters never reach the Talk flow, so they /// are asked once right after signing in. Future maybePromptGuardianLoginNotifications(BuildContext context) => _maybePrompt(context, _PermissionPrompt.guardianLogin); /// Second chance with context: explains the request once more on the first /// visit of the parent letters when the permission is still missing. Future maybePromptParentLetterNotifications(BuildContext context) => _maybePrompt(context, _PermissionPrompt.parentLettersVisit); bool _promptInFlight = false; Future _maybePrompt( BuildContext context, _PermissionPrompt prompt, ) async { final module = prompt.module; if (module != null && !AppModule.isAvailableFor(module, SessionManager().current)) { return; } final settings = context.read(); final notificationSettings = settings.val().notificationSettings; // Already handled once, or the user opted out of push entirely. if (prompt.wasShown(notificationSettings)) return; if (!notificationSettings.enabled) return; // Capabilities may still be loading on a fresh cold start; retry on the next // occasion instead of burning the one-shot flag. if (!context.read().canReceivePushNotifications) return; if (_promptInFlight) return; _promptInFlight = true; try { // Users who already granted the permission: register silently and mark the // prompt as handled without showing any dialog. if (await PushRegistration.isOsPermissionGranted()) { prompt.markShown(settings.val(write: true).notificationSettings); unawaited(PushRegistration().register()); return; } if (!context.mounted) return; if (prompt.spentOnDisplay) { prompt.markShown(settings.val(write: true).notificationSettings); } await showDialog( context: context, builder: ConfirmDialog( icon: Icons.notifications_active_outlined, title: 'Benachrichtigungen aktivieren', content: prompt.explanation, confirmButton: 'Weiter', cancelButton: null, onConfirm: () => unawaited(_requestPermission(context, settings, prompt)), ).build, ); } finally { _promptInFlight = false; } } Future _requestPermission( BuildContext context, SettingsCubit settings, _PermissionPrompt prompt, ) async { final granted = await PushRegistration.requestOsPermission(); // Mark handled regardless of the outcome — the user can re-enable later via // the system settings; we don't want to prompt again on the next occasion. prompt.markShown(settings.val(write: true).notificationSettings); if (granted) { unawaited(PushRegistration().register()); return; } log('Push: notification permission declined on ${prompt.name}'); if (!context.mounted) return; ConfirmDialog( icon: Icons.notifications_off_outlined, title: 'Benachrichtigungen deaktiviert', content: prompt.declinedNote, confirmButton: 'Einstellungen öffnen', cancelButton: 'Später', onConfirm: () => AppSettings.openAppSettings(type: AppSettingsType.notification), ).asDialog(context); }