191 lines
6.6 KiB
Dart
191 lines
6.6 KiB
Dart
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<void> 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<void> 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<void> maybePromptParentLetterNotifications(BuildContext context) =>
|
|
_maybePrompt(context, _PermissionPrompt.parentLettersVisit);
|
|
|
|
bool _promptInFlight = false;
|
|
|
|
Future<void> _maybePrompt(
|
|
BuildContext context,
|
|
_PermissionPrompt prompt,
|
|
) async {
|
|
final module = prompt.module;
|
|
if (module != null &&
|
|
!AppModule.isAvailableFor(module, SessionManager().current)) {
|
|
return;
|
|
}
|
|
|
|
final settings = context.read<SettingsCubit>();
|
|
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<CapabilitiesCubit>().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<void>(
|
|
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<void> _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);
|
|
}
|