DEV3 Technologies home

Push notification permission flow

Ask for notification permission at a good moment with a pre-prompt, then handle denied and permanently denied states gracefully.

FlutterNotificationsflutterpushpermissionnotifications

Code

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

enum PushState { granted, askable, blocked }

class PushPermission {
  /// Reads the current state without showing any system dialog.
  static Future<PushState> check() async {
    final s = await Permission.notification.status;
    if (s.isGranted || s.isProvisional) return PushState.granted;
    if (s.isPermanentlyDenied) return PushState.blocked;
    return PushState.askable;
  }

  /// Shows our own explanation first; the system dialog is only spent on a yes.
  static Future<PushState> request(BuildContext context) async {
    final current = await check();
    if (current != PushState.askable || !context.mounted) {
      if (current == PushState.blocked && context.mounted) await _offerSettings(context);
      return current;
    }
    final agree = await showDialog<bool>(
      context: context,
      builder: (c) => AlertDialog(
        icon: const Icon(Icons.notifications_active_outlined),
        title: const Text('Stay up to date'),
        content: const Text('Allow notifications so we can tell you when your ride or order status changes.'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('Not now')),
          FilledButton(onPressed: () => Navigator.pop(c, true), child: const Text('Allow')),
        ],
      ),
    );
    if (agree != true) return PushState.askable; // keep the system prompt for later
    final result = await Permission.notification.request();
    return result.isGranted || result.isProvisional
        ? PushState.granted
        : (result.isPermanentlyDenied ? PushState.blocked : PushState.askable);
  }

  static Future<void> _offerSettings(BuildContext context) async {
    final open = await showDialog<bool>(
      context: context,
      builder: (c) => AlertDialog(
        title: const Text('Notifications are off'),
        content: const Text('You can turn them on in system settings.'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('Cancel')),
          FilledButton(onPressed: () => Navigator.pop(c, true), child: const Text('Open settings')),
        ],
      ),
    );
    if (open == true) await openAppSettings();
  }
}

Needs: permission_handler@^11.3.0

How it works

What it solves

The system notification prompt can usually be shown only once. If it appears at launch with no context, many people tap Don't allow, and the app can never ask again.

How it works

check reads the permission status silently and collapses the many plugin states into three: granted, askable and blocked. request first shows an in-app dialog that explains the value of notifications. Only if the user agrees does it trigger the real system prompt, so the one-shot dialog is not wasted on people who would refuse.

If the status is permanently denied, the only way forward is system settings, so the flow offers a dialog with an Open settings button that calls openAppSettings. A result of askable after a decline means you may try again at another good moment.

Adapting it

Call request after a relevant action, such as booking the first ride, not on the splash screen. After granting, register the device token with your push service. Store the time of the last decline to avoid nagging more often than every few weeks.

Pitfalls

  • Android 13 and newer requires the notification permission declared in the manifest; older versions grant it implicitly.
  • iOS needs push capability set up in the project.
  • Guard every context use after an await with context.mounted.

How to use it

Add permission_handler to pubspec.yaml, enable the notification permission for each platform as described by the package, and copy the file to lib/notifications/.

final state = await PushPermission.request(context);
if (state == PushState.granted) registerDeviceToken();

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets