How do I to navigate to a page after notification tap on flutter

for the last few days Ive been trying to set up such that the firebase FCM notifications I get can reroute users to the correct page that is set in the payload. I have tried a few things but have failed.

  1. Try to use the Nav key.
    I have a rootNavigatorKey but when I try to access it via main.rootNavigatorKey or via passing it thought methods. The value of rootNavigatorKey is null.

  2. Try to use context.
    I pass in context but sometime it glitches. meaning that it will continously will route to page X. so when I try to pop on said page X, it leads me to page X. I may have to pop a few times for me to go to the actual page that i want to.

     [log] Notification received: New Employee 12
     [log] Notification received: New Employee 13
     [log] Notification received: New Employee 14
     [log] Notification received: New Employee 15
     [log] Notification received: New Employee 16
    

    The intresting thing here is some time even with the slew of messages (Notification received), I at times just do 1 pop and its perfect. (I assume there could be some issues with initNotif when nav between pages). P.S. I tried to init in the main but then context come up as null.

    I tried to use a MUTEX as well as a deboucer around the context.push but neither worked, code ran thru the checks.

  3. Try to use addPostFrameCallback
    This was promising but after finding the issuse above (#2). This would cause delays in pops and thus more issues.

I am using go_router: 12.1.3 due to PopScope Issue on Android

This is my notification class:

const appLinkLength = "www.mazaar.co".length;

Future<void> handler(RemoteMessage message) async {
  developer.log("Handling background message: ${message.messageId}");
}

class FirebaseNotificationAPI {
  final FirebaseMessaging fm = FirebaseMessaging.instance;
  final AndroidNotificationChannel androidChannel =
      const AndroidNotificationChannel(
    "high_importance_channel",
    "High Importance Notifications",
    description: "Notification that you must see",
    importance: Importance.max,
  );

  final FlutterLocalNotificationsPlugin localNotification =
      FlutterLocalNotificationsPlugin();

  Future<void> initNotif(BuildContext context) async {
    // Request permissions for iOS/Android.
    await fm.requestPermission();
    final token = await fm.getToken();
    developer.log("FCM Token: $token");

    // Initialize local and push notifications.
    await initPushNotif(context);
    await initLocalNotif(context);
  }

  Future<void> handleMessage(
      BuildContext context, RemoteMessage? message) async {
    if (message == null) return;
    developer.log("Notification received: ${message.notification?.title}");
    final deepLink =
        message.data["DeepLink"].toString().substring(appLinkLength);
    await GoRouter.of(context).push(deepLink);
  }

  Future<void> initLocalNotif(BuildContext context) async {
    const androidSettings =
        AndroidInitializationSettings('@drawable/notification_icon');
    const settings = InitializationSettings(android: androidSettings);

    await localNotification.initialize(
      settings,
      onDidReceiveNotificationResponse: (details) async {
        developer.log("Notification response details: $details");
        final deepLink = jsonDecode(details.payload!)["data"]!["DeepLink"]
            .toString()
            .substring(appLinkLength);
        ();
        await GoRouter.of(context).push(deepLink);
      },
    );

    // Create the notification channel on Android.
    final platform = localNotification.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();

    if (platform != null) {
      await platform.createNotificationChannel(androidChannel);
    }
  }

  Future<void> initPushNotif(BuildContext context) async {
    // Handle the initial notification if the app is opened via a notification.
    fm.getInitialMessage().then(
      (message) {
        handleMessage(context, message);
      },
    );

    // Handle messages when the app is in the foreground.
    FirebaseMessaging.onMessage.listen((message) {
      final notification = message.notification;
      if (notification == null) return;

      developer.log("Foreground notification: ${notification.title}");
      localNotification.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            androidChannel.id,
            androidChannel.name,
            channelDescription: androidChannel.description,
            icon: '@drawable/notification_icon',
          ),
        ),
        payload: jsonEncode(message.toMap()),
      );
    });

    // Handle messages when the app is reopened via a notification.
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      handleMessage(context, message);
    });

    // Handle background messages.
    FirebaseMessaging.onBackgroundMessage(handler);
  }
}

this is my main

final log = Logger('JumpLogger');
final rootNavigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  //to load production env variables replace '.env.dev' with '.env.prod'
  const stage = String.fromEnvironment("STAGE");
  if (stage == 'prod') {
    try {
      await dotenv.load(fileName: ".env.prod");
      if (dotenv.env.isEmpty) {
        throw Exception("PROD env file is empty.");
      }
      Environment.updateStageName("prod");
      developer.log('ENVIRONMENT:PROD');
    } catch (e) {
      developer.log("COULDN'T GET PROD ENV");
    }
  } else if (stage == 'dev') {
    try {
      await dotenv.load(fileName: ".env.dev");
      if (dotenv.env.isEmpty) {
        throw Exception("DEV env file is empty.");
      }
      Environment.updateStageName("dev");
      developer.log('ENVIRONMENT:DEV');
    } catch (e) {
      developer.log("COULDN'T GET DEV ENV");
    }
  } else {
    throw Exception('Could not load any environment file');
  }
  await Firebase.initializeApp(
    name: Environment.getStageName(),
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await fetchAndActivateConfig();
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };
  // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics

  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };
  Logger.root.level = Level.ALL; // defaults to Level.INFO
  Logger.root.onRecord.listen((record) {
    developer.log('${record.level.name}: ${record.time}: ${record.message}');
  });

  String sellerOrConsumerActive =
      FirebaseRemoteConfig.instance.getString('SELLER_OR_CONSUMER_ACTIVE');
  Environment.updateSellerOrConsumerActive(sellerOrConsumerActive);

  String appServer = FirebaseRemoteConfig.instance.getString('APP_SERVER');
  Environment.updateServerEndpoint(appServer);

  String chatGPTAPIKey = FirebaseRemoteConfig.instance.getString('CHATGPT_KEY');
  Environment.updateChatGPTAPIKey(chatGPTAPIKey);

  String googleNlpAPIKey =
      FirebaseRemoteConfig.instance.getString('GOOGLE_KEY');
  Environment.updateGoogleNlpAPIKey(googleNlpAPIKey);

  String razorPayAPIId =
      FirebaseRemoteConfig.instance.getString('RAZOR_PAY_KEY_ID');
  Environment.updateRazorPayAPIId(razorPayAPIId);

  String razorPayAPISecret =
      FirebaseRemoteConfig.instance.getString('RAZOR_PAY_KEY_SECRET');
  Environment.updateRazorPayAPISecret(razorPayAPISecret);

  Environment.updateCert(await getBuildCert());
  PackageInfo packageInfo = await PackageInfo.fromPlatform();
  Environment.updatePackageName(packageInfo.packageName);

  await CategoryUtils.loadCategories();

  // Make the BuyCart API call before running the app
  try {
    BuyCart buyCart =
        await BuyCartAPI.getBuyCartForMain(await Auth.getTokenForMain());
    Feature.setActiveCart(buyCart);
    developer.log('Got and set cart to: $buyCart');
  } catch (e) {
    developer.log('Could not fetch buy cart: $e');
  }

  runApp(MyApp(sellerOrConsumerActive: sellerOrConsumerActive));
}

Widget getHomePage(String sellerOrConsumerActive) {
  developer.log('[Main] SellerOrConsumerActive: $sellerOrConsumerActive');
  switch (sellerOrConsumerActive) {
    case 'SELLER_ONLY':
      return const LoginPage();
    case 'SELLER_AND_CONSUMER':
      return const HomePage();
    default:
      throw Exception(
          "Unknown value for SellerOrConsumerActive: $sellerOrConsumerActive");
  }
}

Future<String> getBuildCert() async {
  const platform = MethodChannel('com.mazaar.frontend/keys');
  try {
    final String buildCert = await platform.invokeMethod('getBuildCert');
    return buildCert;
  } catch (e) {
    developer.log("Failed to get certs: $e");
    return "";
  }
}

Future<void> fetchAndActivateConfig() async {
  final remoteConfig = FirebaseRemoteConfig.instance;
  await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(minutes: 1),
    minimumFetchInterval: Duration(
        seconds: int.parse(dotenv.env['FIREBASE_REMOTE_CONFIG_DURATION']!)),
  ));

  try {
    // Fetch and activate
    await remoteConfig.fetchAndActivate();
  } catch (e) {
    developer.log('Failed to fetch remote config: $e');
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.sellerOrConsumerActive});
  final String sellerOrConsumerActive;

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: getRouter(sellerOrConsumerActive),
      theme: CustomTheme.themeData,
      debugShowCheckedModeBanner: false,
    );
  }
}

GoRouter getRouter(sellerOrConsumerActive) {
  final Widget homePage = getHomePage(Environment.getSellerOrConsumerActive());
  final shellNavigatorKey = GlobalKey<NavigatorState>();

  return GoRouter(
    navigatorKey: rootNavigatorKey,
    initialLocation: RoutingConstants.root.path,
    routes: <RouteBase>[
      ShellRoute(
        navigatorKey: shellNavigatorKey,
        routes: [
          GoRoute(
            name: RoutingConstants.address.name,
            path: RoutingConstants.address.path,
            pageBuilder: (context, state) => NoTransitionPage<void>(
              key: state.pageKey,
              child: const ChangeAddressPage(),
            ),
          ),
          GoRoute(
              name: RoutingConstants.root.name,
              path: RoutingConstants.root.path,
              pageBuilder: (context, state) => NoTransitionPage<void>(
                    key: state.pageKey,
                    child: homePage,
                  ),
              routes: [
                GoRoute(
                  name: RoutingConstants.item.name,
                  path: RoutingConstants.item.subroute,
                  pageBuilder: (context, state) => NoTransitionPage<void>(
                    key: state.pageKey,
                    child: ItemPage(
                      itemId: state.pathParameters['itemId'].toString(),
                    ),
                  ),
                ),
             ...
        ],
        builder: (context, state, child) {
          return child;
        },
      )
    ],
  );
}

this is where I call FirebaseNotificaitionAPI.initNotif(...)

class ProfilePage extends StatelessWidget {
...
  Future<Map<String, dynamic>> getProfileData(BuildContext context) async {
    await FirebaseNotificationAPI().initNotif(context);
    final user = firebaseAuth.currentUser;
    final storeExists = await checkIfStoreExists(context);
    return {
      'user': user,
      'storeExists': storeExists,
    };
  }


  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false || Environment.isSellerAndConsumer(),
      child: ScaffoldWrapper(
        showBack: false || Environment.isSellerAndConsumer(),
        child: FutureBuilder<Map<String, dynamic>>(
          future: getProfileData(context),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(child: CircularProgressIndicator());
            } else if (snapshot.hasError) {
              return const ErrorDisplay(
                errorMessage: 'Error could not load profile data.',
              );
            } else if (!snapshot.hasData) {
              return const ErrorDisplay(
                errorMessage: 'No data available.',
              );
            } else {
              final user = snapshot.data!['user'] as User?;
              final storeExists = snapshot.data!['storeExists'] as bool;
              final profilePictureUrl = user?.photoURL ?? image;

              return Column(
                children: [
                  Environment.isSellerAndConsumer()
                      ? const HeaderSearchBar(
                          initText: '',
                        )
                      : Container(),
                  Padding(
                    padding: const EdgeInsetsDirectional.fromSTEB(10, 0, 10, 0),
                    child: ListView(
                      ...
                      children: [
                        profilePicture(context, profilePictureUrl),
                        welcomeTitle(user),
                        storeExists
                            ? seeYourStoreButton(context)
                            : businessButton(context),
                        signOutButton(context),
                        manageDataButton(context),
                      ],
                    ),
                  ),
                ],
              );
            }
          },
        ),
      ),
    );
  }

...

and this one as well


  @override
  void initState() {
    super.initState();
    _firebaseInit = FirebaseNotificationAPI().initNotif(context);
  }

  @override
  Widget build(BuildContext context) {
    final Uri topBannerAdUrl = Uri.parse('https://flutter.dev');

    return ScaffoldWrapper(
      showBack: false,
      child: Column(
        children: [
          const HeaderSearchBar(
            initText: '',
          ),
          FutureBuilder<void>(
            future: _firebaseInit,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else {
                return Expanded(
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        GestureDetector(
                          onTap: () => _launchUrl(topBannerAdUrl),
                          child: const ImageLoader(imageUri: imageUrl),
                        ),
                        const Padding(padding: EdgeInsets.only(top: 10)),
                        Column(
                          children: List.generate(
                            itemTitles.length,
                            (index) {
                              String title = (index < itemTitles.length)
                                  ? itemTitles[index]
                                  : 'Hot Items';
                              return ItemRow(title: title);
                            },
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}
1 Like

Using Navigator 2.0.

Step 1: Have notification service to track the push notifications, and expose API that provides a stream emitting the notification information to their listeners.
Step 2: In your central route managing class, subscribe to this stream.
Step 3: Based on the information, set the list of pages and rebuild the router widget through the router delegate.

1 Like

@mike In general, on any forum or SO or even your companies chat or anywhere, posting so much code is probably not a good way to figure out the problem. Trying to make a minimal sample of the problem, small snippets , and what the problem is conceptually would be a better way to obtain solutions vs copy pasting a ton of code and being like here’s my code, read it figure out everything that’s going on and fix my issue

1 Like

In general I agree but we are way more relaxed in this forum.

1 Like

Absolutely my approach. Having this listener placed in your some of your root level widgets allows to have always access to the root navigator.
Another approach that’s not as clean but still practical is storing the navigator key somewhere.
See my unfinished interaction_manager package on pub.

would this work with go router as well?