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.
-
Try to use the Nav key.
I have arootNavigatorKey
but when I try to access it viamain.rootNavigatorKey
or via passing it thought methods. The value ofrootNavigatorKey
is null. -
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 topop
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 1pop
and its perfect. (I assume there could be some issues withinitNotif
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. -
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);
},
),
),
],
),
),
);
}
},
),
],
),
);
}
}