Flutter: OneSignal additionalData on app start

There appears to be a bug in the OneSignal package for Flutter where you cannot reliably use the additionalData on app cold boot. This is my workaround.

Problem

You wanna do something specific in your Flutter app based on the payload with a push notification coming from OneSignal. Problem is, it seems on cold-boot of the app, Flutter hasn't yet initialised its UI and therefore you can't do anything like navigate to a new page base on the payload contents. The workaround I have done is to store the payload for later on in the app initialisation lifecycle.

Please note:

  • I know this isn't applicable for everyone's app structure. If this works for you, great. If you don't like this solution, you're free to ignore it :)
  • The pages PageCalendar and PageCommunity and navigating according to the payload data below are just examples of what I did. Change this example to fit your needs and your app.

Solution

I've reduced the code where possible to make it simple.

  1. I initialise the FlutterBinding stuff..
  2. I await setup local key value storage with GetStorage.
  3. I register OneSignal, with my custom class OneSignalUtils
  4. I wrap my app in a container widget App inside MaterialApp

Notes:

  • If the notification is tapped when the app isn't loaded, my app loads.
  • It registers OneSignal.
  • Then the handler will check if there's a callback registered that pertains to the additionalData.
  • If there's no callback set, the value is saved to storage. Otherwise the callback is called.
  • App() loads up and sets those callbacks in OneSignalUtils. This way, the next time, they'll be called.
  • Pay attention to the comments in method: _checkForPushNotificationOnLoad()
  • You can see how I optionally load one of 2 pages in my app - Community or Calendar.
  • All this code works and is in production in several apps. It may not be the best way, but it works for me. Please don't reply telling me not to use singletons or any other code advice, thanks!

Code....

In main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await setupLocalStorage();
  OneSignalUtils.init();

  runApp(
    MyApp(),
  );
}

Future<bool> setupLocalStorage() async {
  return GetStorage.init();
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "My app!",
      home: App()
    );
  }
}

In App - the wrapper, just a stateful widget:

class App extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AppState();
}

class AppState extends State<App> {

  @override
  void initState() {
    super.initState();

    Future.wait([
      _checkForPushNotificationOnLoad(),
    ]);
  }

  Future<bool> _checkForPushNotificationOnLoad() async {
    // Overwrite the handlers for when the app is in memory.
    OneSignalUtils.registerHandlers(
      communityLoader: _launchCommunityPage,
      calendarLoader: _launchCalendarPage,
    );

    // Check values from when app was first loaded.
    bool loadCommunityPage = StorageService().getLoadCommunityOnLoad();
    bool loadCalendarPage = StorageService().getLoadCalendarOnLoad();

    // Reset values.
    StorageService().setLoadCommunityOnLoad(false);
    StorageService().setLoadCalendarOnLoad(false);

    // Check to load.
    if (loadCommunityPage) {
      _launchCommunityPage();
    } else if (loadCalendarPage) {
      _launchCalendarPage();
    }

    return false;
  }

  void _launchCommunityPage() async {
    Navigator.of(context, rootNavigator: true).push(
      MaterialPageRoute(
        builder: (context) => const PageCommunity(),
      ),
    );
  }

  void _launchCalendarPage() async {
    Navigator.of(context, rootNavigator: true).push(
      MaterialPageRoute(
        builder: (context) => const PageCalendar(),
      ),
    );
  }
}

OneSignalUtils.dart:

import 'dart:io';
import 'package:onesignal_flutter/onesignal_flutter.dart';
import 'package:scy/services/storage.dart';

class OneSignalUtils {
  static void init() {
    if (Platform.isAndroid) {
      // // the SDK will now initialize
      // await OneSignal.shared.consentGranted(true);

      // NOTE: If this isn't working on an Android device, check that it doesn't
      // have any VPN/ad blocking software running.
    }

    OneSignal.shared.setLogLevel(OSLogLevel.verbose, OSLogLevel.none);
    OneSignal.shared.setAppId("Your app Id here");

    if (!Platform.isAndroid) {
      OneSignal.shared
          .promptUserForPushNotificationPermission()
          .then((accepted) {
        OneSignal.shared.disablePush(!accepted);
      });
    }

    // App is in foreground
    OneSignal.shared.setNotificationWillShowInForegroundHandler(
        (OSNotificationReceivedEvent event) {
      // Display Notification, pass null param for not displaying the notification
      event.complete(event.notification);
    });

    registerHandlers();
  }

  static void registerHandlers({
    Function? communityLoader,
    Function? calendarLoader,
  }) {
    // Notification tapped - when app hasn't loaded yet.
    OneSignal.shared
        .setNotificationOpenedHandler((OSNotificationOpenedResult result) {

      if (result.notification.additionalData != null &&
          result.notification.additionalData!.isNotEmpty) {
        result.notification.additionalData!.forEach((key, value) {
          if (key == "page" && value == "community") {
            if (communityLoader != null) {
              communityLoader();
            } else {
              StorageService().setLoadCommunityOnLoad(true);
            }
          } else if (key == "page" && value == "calendar") {
            if (calendarLoader != null) {
              calendarLoader();
            } else {
              StorageService().setLoadCalendarOnLoad(true);
            }
          }
        });
      }
    });
  }
}

StorageService.dart:

import 'dart:convert';
import 'package:get_storage/get_storage.dart';

class StorageService {
  static final StorageService _instance = StorageService._internal();
  final box = GetStorage();

  // using a factory is important
  // because it promises to return _an_ object of this type
  // but it doesn't promise to make a new one.
  factory StorageService() {
    return _instance;
  }

  // This named constructor is the "real" constructor
  // It'll be called exactly once, by the static property assignment above
  // it's also private, so it can only be called in this class.
  StorageService._internal() {
    // initialization logic
  }

  Future<void> clear() async {
    List<Future> list = [
      // push notifications
      box.write("loadCommunityOnLoad", null),
      box.write("loadCalendarOnLoad", null),
    ];
    await Future.wait(list);
  }


  Future<void> setLoadCommunityOnLoad(bool value) async {
    try {
      return box.write("loadCommunityOnLoad", value);
    } catch (e) {
      return Future.sync(() => null);
    }
  }

  Future<void> setLoadCalendarOnLoad(bool value) async {
    try {
      return box.write("loadCalendarOnLoad", value);
    } catch (e) {
      return Future.sync(() => null);
    }
  }

  bool getLoadCommunityOnLoad() {
    bool? data = box.read("loadCommunityOnLoad");
    return (data != null) ? data : false;
  }

  bool getLoadCalendarOnLoad() {
    bool? data = box.read("loadCalendarOnLoad");
    return (data != null) ? data : false;
  }
}

Conclusion

Now when you send a push from OneSignal to your device, the app will successfully handle the payload on cold-boot and then act accordingly.

Hope this helps!

Comments: