implemented a guided notification permission flow triggered on the first Talk visit
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_settings/app_settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -42,16 +45,41 @@ class _PushStatusBody extends StatefulWidget {
|
||||
State<_PushStatusBody> createState() => _PushStatusBodyState();
|
||||
}
|
||||
|
||||
class _PushStatusBodyState extends State<_PushStatusBody> {
|
||||
class _PushStatusBodyState extends State<_PushStatusBody>
|
||||
with WidgetsBindingObserver {
|
||||
PushStatusReport? _report;
|
||||
bool _busy = false;
|
||||
|
||||
/// Set while the user is in the OS settings — the next `resumed` lifecycle
|
||||
/// event then re-collects the status so a just-changed permission shows up.
|
||||
bool _reloadOnResume = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && _reloadOnResume) {
|
||||
_reloadOnResume = false;
|
||||
unawaited(_load());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openNotificationSettings() async {
|
||||
_reloadOnResume = true;
|
||||
await AppSettings.openAppSettings(type: AppSettingsType.notification);
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final capabilitiesState = widget.capabilities.state;
|
||||
final report = await collectPushStatus(
|
||||
@@ -117,14 +145,24 @@ class _PushStatusBodyState extends State<_PushStatusBody> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...rows.map(
|
||||
(row) => ListTile(
|
||||
...rows.map((row) {
|
||||
// Offer the OS-settings shortcut on the permission row whenever it
|
||||
// isn't granted — after a denial that's the only place to fix it.
|
||||
final canOpenSettings =
|
||||
row.opensNotificationSettings && row.state != PushCheck.ok;
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: _stateIcon(row.state, theme),
|
||||
title: Text(row.label),
|
||||
subtitle: row.detail == null ? null : Text(row.detail!),
|
||||
),
|
||||
),
|
||||
trailing: canOpenSettings
|
||||
? TextButton(
|
||||
onPressed: _openNotificationSettings,
|
||||
child: const Text('Einstellungen'),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
if (report.general.lastRegistrationAt != null ||
|
||||
report.talk.lastRegistrationAt != null)
|
||||
const Divider(height: 1),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_split_view/flutter_split_view.dart';
|
||||
@@ -11,6 +13,7 @@ import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'join_chat.dart';
|
||||
import 'notification_permission_prompt.dart';
|
||||
import 'search_chat.dart';
|
||||
import 'widgets/chat_tile.dart';
|
||||
import 'widgets/split_view_placeholder.dart';
|
||||
@@ -44,6 +47,7 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_maybeOpenPendingChat();
|
||||
unawaited(maybePromptTalkNotifications(context));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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 '../../../push/push_registration.dart';
|
||||
import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
|
||||
/// 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) async {
|
||||
final settings = context.read<SettingsCubit>();
|
||||
final notificationSettings = settings.val().notificationSettings;
|
||||
|
||||
// Already handled once, or the user opted out of push entirely.
|
||||
if (notificationSettings.talkPermissionPromptShown) return;
|
||||
if (!notificationSettings.enabled) return;
|
||||
|
||||
// Capabilities may still be loading on a fresh cold start; retry on the next
|
||||
// Talk visit instead of burning the one-shot flag.
|
||||
if (!context.read<CapabilitiesCubit>().canReceivePushNotifications) return;
|
||||
|
||||
// Existing users who already granted the permission: register silently and
|
||||
// mark the prompt as handled without showing any dialog.
|
||||
if (await PushRegistration.isOsPermissionGranted()) {
|
||||
settings.val(write: true).notificationSettings.talkPermissionPromptShown =
|
||||
true;
|
||||
unawaited(PushRegistration().register());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ConfirmDialog(
|
||||
icon: Icons.notifications_active_outlined,
|
||||
title: 'Benachrichtigungen aktivieren',
|
||||
content:
|
||||
'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.',
|
||||
confirmButton: 'Weiter',
|
||||
cancelButton: null,
|
||||
onConfirm: () => unawaited(_requestPermission(context, settings)),
|
||||
).asDialog(context);
|
||||
}
|
||||
|
||||
Future<void> _requestPermission(
|
||||
BuildContext context,
|
||||
SettingsCubit settings,
|
||||
) 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 Talk visit.
|
||||
settings.val(write: true).notificationSettings.talkPermissionPromptShown =
|
||||
true;
|
||||
|
||||
if (granted) {
|
||||
unawaited(PushRegistration().register());
|
||||
return;
|
||||
}
|
||||
|
||||
log('Push: notification permission declined on first Talk visit');
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ConfirmDialog(
|
||||
icon: Icons.notifications_off_outlined,
|
||||
title: 'Benachrichtigungen deaktiviert',
|
||||
content:
|
||||
'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.',
|
||||
confirmButton: 'Einstellungen öffnen',
|
||||
cancelButton: 'Später',
|
||||
onConfirm: () =>
|
||||
AppSettings.openAppSettings(type: AppSettingsType.notification),
|
||||
).asDialog(context);
|
||||
}
|
||||
Reference in New Issue
Block a user