You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

8.0 KiB

Guide: Flutter Integration for Push Notifications & Dynamic Routing

This document serves as the official technical integration guide for Flutter developers to handle Firebase Cloud Messages (FCM) sent by the backend. It outlines the payload structure, routing schema, and provides sample Dart implementation snippets using GoRouter.


1. Firebase Cloud Messaging (FCM) Payload Structure

Every push notification sent by the backend contains a standardized data payload. This payload provides the mobile app with necessary metadata to resolve target screens and dynamically navigate the user.

Example JSON Payload

{
  "to": "FCM_DEVICE_TOKEN",
  "notification": {
    "title": "Доступ к курсу активирован",
    "body": "Ваш доступ к курсу «Курс 1» успешно активирован."
  },
  "data": {
    "type": "course_access_granted",
    "action": "navigate",
    "navigate_to": "/course_info?slug=course-1",
    "course_id": "12",
    "room_id": "8",
    "session_id": "5"
  }
}

Key Properties in data Payload

  • type (String): Indicates the unique notification trigger type (e.g. lesson_completed, teacher_reply_private).
  • action (String): Defines the response action when a user clicks the notification. Currently defaults to "navigate".
  • navigate_to (String): A GoRouter path ready for execution (contains query parameters, e.g. /course_info?slug=slug-value).
  • Dynamic IDs (e.g., course_id, room_id, session_id): Additional metadata payload attributes that can be used if custom widgets need parameters directly.

2. Notification Types & Navigation Mapping

Below is the complete registry of all 18 system notification types, their dynamic parameters, and the target routing paths:

Notification Type (type) Action / Event Target Route (navigate_to) Dynamic Parameters in data
course_registered Enrollment confirmation /course_info?slug={course_slug} course_id
course_access_granted Admin activated course access /course_info?slug={course_slug} course_id
lesson_completed Student completed lesson /lesson?slug={course_slug} course_id, lesson_id
course_completed Student finished whole course /course_info?slug={course_slug} course_id
new_course_weekly Weekly newly registered courses /course_info?slug={course_slug} course_id
low_quiz_result Recommendation to retake quiz /course_info/quiz?slug={course_slug} course_id, quiz_id
live_class_rescheduled Rescheduled live class /course_info?slug={course_slug} session_id, course_id
live_class_cancelled Cancelled live session /course_info?slug={course_slug} session_id, course_id
live_recording_available Live class recording uploaded /course_info?slug={course_slug} session_id, course_id
live_class_reminder Reminder before live session /course_info?slug={course_slug} session_id, course_id
missed_live_sessions User missed 2 live sessions /course_info?slug={course_slug} course_id
teacher_reply Teacher reply in general chat /chat/chat_details?room_id={room_id} room_id, message_id
teacher_reply_private Teacher reply in private room /chat/chat_details?room_id={room_id} room_id, message_id
teacher_reply_course Teacher message in course chat /chat/chat_details?room_id={room_id} room_id, message_id
support_reply Support consultant message /chat/chat_details?room_id={room_id} room_id, message_id
payment_successful Course payment success /profile/transactions course_id
payment_failed Course payment failure /profile/transactions course_id
refund_completed Refund completed /profile/transactions course_id
student_inactivity Weekly inactivity warning /home -

3. Flutter Implementation Guide

To ensure navigation works in all app states (terminated, background, and foreground), follow the implementation patterns below.

A. Initializing and Handling Click Events

The app must check if it was opened from a terminated state (cold start) or was already running in the background.

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class NotificationService {
  final GoRouter router;

  NotificationService(this.router);

  Future<void> initialize() async {
    // 1. App was terminated and opened by clicking a notification
    RemoteMessage? initialMessage =
        await FirebaseMessaging.instance.getInitialMessage();
    if (initialMessage != null) {
      _handleNotificationClick(initialMessage);
    }

    // 2. App is in the background and opened by clicking a notification
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      _handleNotificationClick(message);
    });
  }

  void _handleNotificationClick(RemoteMessage message) {
    final data = message.data;
    if (data.containsKey('action') && data['action'] == 'navigate') {
      final String? route = data['navigate_to'];
      if (route != null && route.isNotEmpty) {
        // Execute dynamic GoRouter redirect
        router.push(route);
      }
    }
  }
}

B. Safe Route Integration with GoRouter

Make sure your GoRouter configurations matches paths to parse query parameters (such as slug and room_id) cleanly:

final GoRouter appRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    GoRoute(
      path: '/home',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/course_info',
      builder: (context, state) {
        // Parse 'slug' query parameter (e.g. /course_info?slug=my-course-slug)
        final slug = state.uri.queryParameters['slug'] ?? '';
        return CourseDetailScreen(courseSlug: slug);
      },
    ),
    GoRoute(
      path: '/lesson',
      builder: (context, state) {
        final slug = state.uri.queryParameters['slug'] ?? '';
        return LessonDetailScreen(courseSlug: slug);
      },
    ),
    GoRoute(
      path: '/course_info/quiz',
      builder: (context, state) {
        final slug = state.uri.queryParameters['slug'] ?? '';
        return QuizScreen(courseSlug: slug);
      },
    ),
    GoRoute(
      path: '/chat/chat_details',
      builder: (context, state) {
        final roomId = state.uri.queryParameters['room_id'] ?? '';
        return ChatDetailsScreen(roomId: roomId);
      },
    ),
    GoRoute(
      path: '/profile/transactions',
      builder: (context, state) => const TransactionHistoryScreen(),
    ),
  ],
);

C. Handling Foreground Notifications (Optional)

If a user is inside the app and receives a notification, you may want to display an in-app banner instead of navigating immediately. Use the following stream:

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  // Show in-app banner or toast notification.
  // Do NOT navigate immediately to avoid breaking user workflow.
  debugPrint("Foreground message received: ${message.notification?.title}");
});

4. Troubleshooting & Best Practices

  1. Cold Start Timing Delay: When launching the app from a terminated state, GoRouter might not be fully initialized when getInitialMessage resolves. If redirection fails during cold-starts, defer execution by a tick using WidgetsBinding.instance.addPostFrameCallback:
    WidgetsBinding.instance.addPostFrameCallback((_) {
      router.push(route);
    });
    
  2. Dynamic Placeholders Fallback: If the backend has not yet populated a template or data parameter, query parameters will contain empty placeholders. Implement default screen fallbacks on your pages when slug or room_id is empty.
  3. Registering Device Tokens: Ensure the device registers/updates the token dynamically on user login or token refresh via the /api/account/update-fcm/ endpoint.