From ba244b6debf3c6d14bcd86ffdf14552f76abb530 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 22 Sep 2026 15:35:30 -0500 Subject: [PATCH 1/6] fix: [SDK-5332] reject null and empty identity strings Blank login, app id, language, alias, email, sms, tag key, trigger key, and custom event names were forwarded to native. One helper now drops those calls. Co-authored-by: Cursor --- lib/onesignal_flutter.dart | 3 +++ lib/src/inappmessages.dart | 15 +++++++------ lib/src/user.dart | 37 +++++++++++++++++++++++++++++++++ lib/src/utils.dart | 8 +++++++ test/onesignalflutter_test.dart | 18 ++++++++++++++-- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index 922ea569..f54b17cc 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -9,6 +9,7 @@ import 'package:onesignal_flutter/src/location.dart'; import 'package:onesignal_flutter/src/notifications.dart'; import 'package:onesignal_flutter/src/session.dart'; import 'package:onesignal_flutter/src/user.dart'; +import 'package:onesignal_flutter/src/utils.dart'; export 'src/defines.dart'; export 'src/inappmessage.dart'; @@ -48,6 +49,7 @@ class OneSignal { /// The initializer accepts an [appId] which the developer can get /// from the OneSignal consoleas well as a dictonary of [launchOptions] static Future initialize(String appId) async { + if (rejectNullOrEmpty(appId, 'initialize: appId')) return; await _channel.invokeMethod('OneSignal#initialize', {'appId': appId}); await Future.wait([ InAppMessages.lifecycleInit(), @@ -62,6 +64,7 @@ class OneSignal { /// The act of logging a user into the OneSignal SDK will switch the /// user context to that specific user. static Future login(String externalId) async { + if (rejectNullOrEmpty(externalId, 'login: externalId')) return; return await _channel .invokeMethod('OneSignal#login', {'externalId': externalId}); } diff --git a/lib/src/inappmessages.dart b/lib/src/inappmessages.dart index ca10b3e9..7121fba1 100644 --- a/lib/src/inappmessages.dart +++ b/lib/src/inappmessages.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:onesignal_flutter/src/inappmessage.dart'; +import 'package:onesignal_flutter/src/utils.dart'; typedef void OnClickInAppMessageListener(OSInAppMessageClickEvent event); @@ -38,24 +39,26 @@ class OneSignalInAppMessages { /// Adds a single key, value trigger, which will trigger an in app message /// if one exists matching the specific trigger added Future addTrigger(String key, String value) async { + if (rejectNullOrEmpty(key, 'addTrigger: key')) return; return await _channel.invokeMethod("OneSignal#addTrigger", {key: value}); } - /// Adds one or more key, value triggers, which will trigger in app messages - /// (one at a time) if any exist matching the specific triggers added Future addTriggers(Map triggers) async { + for (final key in triggers.keys) { + if (rejectNullOrEmpty(key, 'addTriggers: key')) return; + } return await _channel.invokeMethod("OneSignal#addTriggers", triggers); } - /// Remove a single key, value trigger to prevent an in app message from - /// showing with that trigger Future removeTrigger(String key) async { + if (rejectNullOrEmpty(key, 'removeTrigger: key')) return; return await _channel.invokeMethod("OneSignal#removeTrigger", key); } - /// Remove one or more key, value triggers to prevent any in app messages - /// from showing with those triggers Future removeTriggers(List keys) async { + for (final key in keys) { + if (rejectNullOrEmpty(key, 'removeTriggers: key')) return; + } return await _channel.invokeMethod("OneSignal#removeTriggers", keys); } diff --git a/lib/src/user.dart b/lib/src/user.dart index cc8dfa43..448bc078 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/src/pushsubscription.dart'; import 'package:onesignal_flutter/src/utils.dart'; @@ -62,6 +63,7 @@ class OneSignalUser { /// Sets the user's language to [language] this also applies to /// the email and/or SMS player if those are logged in on the device. Future setLanguage(String language) async { + if (rejectNullOrEmpty(language, 'setLanguage: language')) return; return await _channel.invokeMethod("OneSignal#setLanguage", { 'language': language, }); @@ -72,6 +74,10 @@ class OneSignalUser { /// If this [alias] label already exists on this user, /// it will be overwritten with the new alias [id]. Future addAlias(String alias, dynamic id) async { + if (rejectNullOrEmpty(alias, 'addAlias: label') || + rejectNullOrEmpty(id, 'addAlias: id')) { + return; + } return await this.addAliases({alias: id}); } @@ -79,16 +85,26 @@ class OneSignalUser { /// /// If any alias already exists, it will be overwritten to the new values. Future addAliases(Map aliases) async { + for (final label in aliases.keys) { + if (rejectNullOrEmpty(label, 'addAliases: label') || + rejectNullOrEmpty(aliases[label], 'addAliases: id')) { + return; + } + } return await _channel.invokeMethod("OneSignal#addAliases", aliases); } /// Remove an [alias] from the current user. Future removeAlias(String label) async { + if (rejectNullOrEmpty(label, 'removeAlias: label')) return; return await this.removeAliases([label]); } /// Remove [aliases] from the current user. Future removeAliases(List aliases) async { + for (final label in aliases) { + if (rejectNullOrEmpty(label, 'removeAliases: label')) return; + } return await _channel.invokeMethod("OneSignal#removeAliases", aliases); } @@ -98,6 +114,11 @@ class OneSignalUser { /// specific users and/or personalizing messages. If the tag [key] already /// exists, it will be replaced with the [value] provided here. Future addTagWithKey(String key, dynamic value) async { + if (rejectNullOrEmpty(key, 'addTag: key')) return; + if (value == null) { + debugPrint('OneSignal: addTag: value is required'); + return; + } return await this.addTags({key: value.toString()}); } @@ -107,6 +128,13 @@ class OneSignalUser { /// specific users and/or personalizing messages. If the tag key already /// exists, it will be replaced with the value provided here. Future addTags(Map tags) async { + for (final key in tags.keys) { + if (rejectNullOrEmpty(key, 'addTags: key')) return; + if (tags[key] == null) { + debugPrint('OneSignal: addTags: value is required'); + return; + } + } tags.forEach((key, value) { tags[key] = value.toString(); }); @@ -115,11 +143,15 @@ class OneSignalUser { /// Remove the data tag with the provided [key] from the current user. Future removeTag(String key) async { + if (rejectNullOrEmpty(key, 'removeTag: key')) return; return await this.removeTags([key]); } /// Remove multiple [tags] with the provided keys from the current user. Future removeTags(List tags) async { + for (final key in tags) { + if (rejectNullOrEmpty(key, 'removeTags: key')) return; + } return await _channel.invokeMethod("OneSignal#removeTags", tags); } @@ -133,6 +165,7 @@ class OneSignalUser { /// Add a new [email] subscription to the current user. Future addEmail(String email) async { + if (rejectNullOrEmpty(email, 'addEmail: email')) return; return await _channel.invokeMethod("OneSignal#addEmail", email); } @@ -141,6 +174,7 @@ class OneSignalUser { /// Returns false if the specified [email] does not exist /// on the user within the SDK, and no request will be made. Future removeEmail(String email) async { + if (rejectNullOrEmpty(email, 'removeEmail: email')) return; return await _channel.invokeMethod("OneSignal#removeEmail", email); } @@ -148,6 +182,7 @@ class OneSignalUser { /// /// Add an SMS subscription by adding an [smsNumber] Future addSms(String smsNumber) async { + if (rejectNullOrEmpty(smsNumber, 'addSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#addSms", smsNumber); } @@ -156,6 +191,7 @@ class OneSignalUser { /// Returns false if the specified [smsNumber] does not /// exist on the user within the SDK, and no request will be made. Future removeSms(String smsNumber) async { + if (rejectNullOrEmpty(smsNumber, 'removeSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#removeSms", smsNumber); } @@ -175,6 +211,7 @@ class OneSignalUser { /// [properties] is an optional map of custom properties associated with the event. Future trackEvent(String name, [Map? properties]) async { + if (rejectNullOrEmpty(name, 'trackEvent: name')) return; return await _channel.invokeMethod("OneSignal#trackEvent", { 'name': name, 'properties': properties, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ef291b54..ba97642b 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,5 +1,13 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; + +bool rejectNullOrEmpty(Object? value, String api) { + if (value is String && value.isNotEmpty) return false; + debugPrint('OneSignal: $api is required'); + return true; +} + /// An abstract class to provide JSON decoding abstract class JSONStringRepresentable { String jsonRepresentation(); diff --git a/test/onesignalflutter_test.dart b/test/onesignalflutter_test.dart index ae7d50cc..56a82d9d 100644 --- a/test/onesignalflutter_test.dart +++ b/test/onesignalflutter_test.dart @@ -30,10 +30,10 @@ void main() { expect(channelController.state.externalId, equals('user-123')); }); - test('login handles empty externalId', () async { + test('login rejects empty externalId', () async { await OneSignal.login(''); - expect(channelController.state.externalId, equals('')); + expect(channelController.state.externalId, isNull); }); }); @@ -136,4 +136,18 @@ void main() { }); }); }); + + group('user strings', () { + test('addTag rejects an empty key', () async { + await OneSignal.User.addTagWithKey('', 'premium'); + + expect(channelController.state.tags, isNull); + }); + + test('addEmail rejects an empty address', () async { + await OneSignal.User.addEmail(''); + + expect(channelController.state.addedEmail, isNull); + }); + }); } From 80c79b204e09d42d2ad6cbcfdf6ba15fcdafee13 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 24 Sep 2026 12:28:42 -0500 Subject: [PATCH 2/6] fix: [SDK-5332] keep setLanguage("") as the device language reset Empty language is the only reset path, so the blank-string guard was a breaking change. Co-authored-by: Cursor --- lib/src/user.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/user.dart b/lib/src/user.dart index 448bc078..d77b7c40 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -63,7 +63,7 @@ class OneSignalUser { /// Sets the user's language to [language] this also applies to /// the email and/or SMS player if those are logged in on the device. Future setLanguage(String language) async { - if (rejectNullOrEmpty(language, 'setLanguage: language')) return; + // Empty string is the reset to the device language. return await _channel.invokeMethod("OneSignal#setLanguage", { 'language': language, }); From 426e56575f467003911b4ff7dd583956677cf76c Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 29 Sep 2026 14:57:59 -0700 Subject: [PATCH 3/6] fix: [SDK-5332] rename input guards to isMissing The checks read as predicates: isMissing, isMissingAny, and hasMissingEntries. --- lib/onesignal_flutter.dart | 4 ++-- lib/src/inappmessages.dart | 8 ++++---- lib/src/user.dart | 30 +++++++++++++++--------------- lib/src/utils.dart | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index f54b17cc..974140db 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -49,7 +49,7 @@ class OneSignal { /// The initializer accepts an [appId] which the developer can get /// from the OneSignal consoleas well as a dictonary of [launchOptions] static Future initialize(String appId) async { - if (rejectNullOrEmpty(appId, 'initialize: appId')) return; + if (isMissing(appId, 'initialize: appId')) return; await _channel.invokeMethod('OneSignal#initialize', {'appId': appId}); await Future.wait([ InAppMessages.lifecycleInit(), @@ -64,7 +64,7 @@ class OneSignal { /// The act of logging a user into the OneSignal SDK will switch the /// user context to that specific user. static Future login(String externalId) async { - if (rejectNullOrEmpty(externalId, 'login: externalId')) return; + if (isMissing(externalId, 'login: externalId')) return; return await _channel .invokeMethod('OneSignal#login', {'externalId': externalId}); } diff --git a/lib/src/inappmessages.dart b/lib/src/inappmessages.dart index 7121fba1..638cce43 100644 --- a/lib/src/inappmessages.dart +++ b/lib/src/inappmessages.dart @@ -39,25 +39,25 @@ class OneSignalInAppMessages { /// Adds a single key, value trigger, which will trigger an in app message /// if one exists matching the specific trigger added Future addTrigger(String key, String value) async { - if (rejectNullOrEmpty(key, 'addTrigger: key')) return; + if (isMissing(key, 'addTrigger: key')) return; return await _channel.invokeMethod("OneSignal#addTrigger", {key: value}); } Future addTriggers(Map triggers) async { for (final key in triggers.keys) { - if (rejectNullOrEmpty(key, 'addTriggers: key')) return; + if (isMissing(key, 'addTriggers: key')) return; } return await _channel.invokeMethod("OneSignal#addTriggers", triggers); } Future removeTrigger(String key) async { - if (rejectNullOrEmpty(key, 'removeTrigger: key')) return; + if (isMissing(key, 'removeTrigger: key')) return; return await _channel.invokeMethod("OneSignal#removeTrigger", key); } Future removeTriggers(List keys) async { for (final key in keys) { - if (rejectNullOrEmpty(key, 'removeTriggers: key')) return; + if (isMissing(key, 'removeTriggers: key')) return; } return await _channel.invokeMethod("OneSignal#removeTriggers", keys); } diff --git a/lib/src/user.dart b/lib/src/user.dart index d77b7c40..9f3fa8dc 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -74,8 +74,8 @@ class OneSignalUser { /// If this [alias] label already exists on this user, /// it will be overwritten with the new alias [id]. Future addAlias(String alias, dynamic id) async { - if (rejectNullOrEmpty(alias, 'addAlias: label') || - rejectNullOrEmpty(id, 'addAlias: id')) { + if (isMissing(alias, 'addAlias: label') || + isMissing(id, 'addAlias: id')) { return; } return await this.addAliases({alias: id}); @@ -86,8 +86,8 @@ class OneSignalUser { /// If any alias already exists, it will be overwritten to the new values. Future addAliases(Map aliases) async { for (final label in aliases.keys) { - if (rejectNullOrEmpty(label, 'addAliases: label') || - rejectNullOrEmpty(aliases[label], 'addAliases: id')) { + if (isMissing(label, 'addAliases: label') || + isMissing(aliases[label], 'addAliases: id')) { return; } } @@ -96,14 +96,14 @@ class OneSignalUser { /// Remove an [alias] from the current user. Future removeAlias(String label) async { - if (rejectNullOrEmpty(label, 'removeAlias: label')) return; + if (isMissing(label, 'removeAlias: label')) return; return await this.removeAliases([label]); } /// Remove [aliases] from the current user. Future removeAliases(List aliases) async { for (final label in aliases) { - if (rejectNullOrEmpty(label, 'removeAliases: label')) return; + if (isMissing(label, 'removeAliases: label')) return; } return await _channel.invokeMethod("OneSignal#removeAliases", aliases); } @@ -114,7 +114,7 @@ class OneSignalUser { /// specific users and/or personalizing messages. If the tag [key] already /// exists, it will be replaced with the [value] provided here. Future addTagWithKey(String key, dynamic value) async { - if (rejectNullOrEmpty(key, 'addTag: key')) return; + if (isMissing(key, 'addTag: key')) return; if (value == null) { debugPrint('OneSignal: addTag: value is required'); return; @@ -129,7 +129,7 @@ class OneSignalUser { /// exists, it will be replaced with the value provided here. Future addTags(Map tags) async { for (final key in tags.keys) { - if (rejectNullOrEmpty(key, 'addTags: key')) return; + if (isMissing(key, 'addTags: key')) return; if (tags[key] == null) { debugPrint('OneSignal: addTags: value is required'); return; @@ -143,14 +143,14 @@ class OneSignalUser { /// Remove the data tag with the provided [key] from the current user. Future removeTag(String key) async { - if (rejectNullOrEmpty(key, 'removeTag: key')) return; + if (isMissing(key, 'removeTag: key')) return; return await this.removeTags([key]); } /// Remove multiple [tags] with the provided keys from the current user. Future removeTags(List tags) async { for (final key in tags) { - if (rejectNullOrEmpty(key, 'removeTags: key')) return; + if (isMissing(key, 'removeTags: key')) return; } return await _channel.invokeMethod("OneSignal#removeTags", tags); } @@ -165,7 +165,7 @@ class OneSignalUser { /// Add a new [email] subscription to the current user. Future addEmail(String email) async { - if (rejectNullOrEmpty(email, 'addEmail: email')) return; + if (isMissing(email, 'addEmail: email')) return; return await _channel.invokeMethod("OneSignal#addEmail", email); } @@ -174,7 +174,7 @@ class OneSignalUser { /// Returns false if the specified [email] does not exist /// on the user within the SDK, and no request will be made. Future removeEmail(String email) async { - if (rejectNullOrEmpty(email, 'removeEmail: email')) return; + if (isMissing(email, 'removeEmail: email')) return; return await _channel.invokeMethod("OneSignal#removeEmail", email); } @@ -182,7 +182,7 @@ class OneSignalUser { /// /// Add an SMS subscription by adding an [smsNumber] Future addSms(String smsNumber) async { - if (rejectNullOrEmpty(smsNumber, 'addSms: smsNumber')) return; + if (isMissing(smsNumber, 'addSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#addSms", smsNumber); } @@ -191,7 +191,7 @@ class OneSignalUser { /// Returns false if the specified [smsNumber] does not /// exist on the user within the SDK, and no request will be made. Future removeSms(String smsNumber) async { - if (rejectNullOrEmpty(smsNumber, 'removeSms: smsNumber')) return; + if (isMissing(smsNumber, 'removeSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#removeSms", smsNumber); } @@ -211,7 +211,7 @@ class OneSignalUser { /// [properties] is an optional map of custom properties associated with the event. Future trackEvent(String name, [Map? properties]) async { - if (rejectNullOrEmpty(name, 'trackEvent: name')) return; + if (isMissing(name, 'trackEvent: name')) return; return await _channel.invokeMethod("OneSignal#trackEvent", { 'name': name, 'properties': properties, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ba97642b..ddef5b0a 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -bool rejectNullOrEmpty(Object? value, String api) { +bool isMissing(Object? value, String api) { if (value is String && value.isNotEmpty) return false; debugPrint('OneSignal: $api is required'); return true; From 689366e4f977797fd7452b63089678b8c4676788 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 2 Oct 2026 22:33:03 -0700 Subject: [PATCH 4/6] fix: [SDK-5332] reject blank/invalid args in more APIs --- lib/src/liveactivities.dart | 24 +++++++++++++++++++++ lib/src/notifications.dart | 5 +++++ lib/src/session.dart | 4 ++++ lib/src/user.dart | 7 +++--- lib/src/utils.dart | 2 +- test/liveactivities_test.dart | 40 +++++++++++++++++++++++++++++++++++ test/notifications_test.dart | 6 ++++++ test/session_test.dart | 18 ++++++---------- 8 files changed, 90 insertions(+), 16 deletions(-) diff --git a/lib/src/liveactivities.dart b/lib/src/liveactivities.dart index 819e496c..a25c46e5 100644 --- a/lib/src/liveactivities.dart +++ b/lib/src/liveactivities.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:onesignal_flutter/src/utils.dart'; class OneSignalLiveActivities { // private channels used to bridge to ObjC/Java @@ -10,6 +11,10 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future enterLiveActivity(String activityId, String token) async { + if (isMissing(activityId, 'enterLiveActivity: activityId') || + isMissing(token, 'enterLiveActivity: token')) { + return; + } if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#enterLiveActivity", {'activityId': activityId, 'token': token}); @@ -21,6 +26,7 @@ class OneSignalLiveActivities { /// Only applies to iOS. @Deprecated('Currently unsupported, avoid using this method.') Future exitLiveActivity(String activityId) async { + if (isMissing(activityId, 'exitLiveActivity: activityId')) return; if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod( "OneSignal#exitLiveActivity", {'activityId': activityId}); @@ -62,6 +68,11 @@ class OneSignalLiveActivities { /// Only applies to iOS. Future startDefault( String activityId, dynamic attributes, dynamic content) async { + if (isMissing(activityId, 'startDefault: activityId') || + !_isMap(attributes, 'startDefault: attributes') || + !_isMap(content, 'startDefault: content')) { + return; + } if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#startDefault", { 'activityId': activityId, @@ -78,6 +89,10 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future setPushToStartToken(String activityType, String token) async { + if (isMissing(activityType, 'setPushToStartToken: activityType') || + isMissing(token, 'setPushToStartToken: token')) { + return; + } if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#setPushToStartToken", {'activityType': activityType, 'token': token}); @@ -90,11 +105,20 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future removePushToStartToken(String activityType) async { + if (isMissing(activityType, 'removePushToStartToken: activityType')) { + return; + } if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod( "OneSignal#removePushToStartToken", {'activityType': activityType}); } } + + bool _isMap(Object? value, String api) { + if (value is Map) return true; + debugPrint('[OneSignal] $api must be a map'); + return false; + } } /// The setup options for [OneSignal.LiveActivities.setupDefault]. diff --git a/lib/src/notifications.dart b/lib/src/notifications.dart index 32e124e6..3952a643 100644 --- a/lib/src/notifications.dart +++ b/lib/src/notifications.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/src/defines.dart'; import 'package:onesignal_flutter/src/notification.dart'; +import 'package:onesignal_flutter/src/utils.dart'; typedef void OnNotificationPermissionChangeObserver(bool permission); @@ -84,6 +85,10 @@ class OneSignalNotifications { /// Removes a grouped notification. Future removeGroupedNotifications(String notificationGroup) async { + if (isMissing( + notificationGroup, 'removeGroupedNotifications: notificationGroup')) { + return; + } if (defaultTargetPlatform == TargetPlatform.android) { return await _channel.invokeMethod("OneSignal#removeGroupedNotifications", {'notificationGroup': notificationGroup}); diff --git a/lib/src/session.dart b/lib/src/session.dart index 541e25c6..596e2f9b 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:onesignal_flutter/src/utils.dart'; class OneSignalSession { // private channels used to bridge to ObjC/Java @@ -8,18 +9,21 @@ class OneSignalSession { /// Send a normal outcome event for the current session and notifications with the attribution window /// Counted each time sent successfully, failed ones will be cached and reattempted in future Future addOutcome(String name) async { + if (isMissing(name, 'addOutcome: name')) return; return await _channel.invokeMethod("OneSignal#addOutcome", name); } /// Send a unique outcome event for the current session and notifications with the attribution window /// Counted once per notification when sent successfully, failed ones will be cached and reattempted in future Future addUniqueOutcome(String name) async { + if (isMissing(name, 'addUniqueOutcome: name')) return; return await _channel.invokeMethod("OneSignal#addUniqueOutcome", name); } /// Send an outcome event with a value for the current session and notifications with the attribution window /// Counted each time sent successfully, failed ones will be cached and reattempted in future Future addOutcomeWithValue(String name, double value) async { + if (isMissing(name, 'addOutcomeWithValue: name')) return; return await _channel.invokeMethod("OneSignal#addOutcomeWithValue", {"outcome_name": name, "outcome_value": value}); } diff --git a/lib/src/user.dart b/lib/src/user.dart index 9f3fa8dc..26dfa978 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -74,8 +74,7 @@ class OneSignalUser { /// If this [alias] label already exists on this user, /// it will be overwritten with the new alias [id]. Future addAlias(String alias, dynamic id) async { - if (isMissing(alias, 'addAlias: label') || - isMissing(id, 'addAlias: id')) { + if (isMissing(alias, 'addAlias: label') || isMissing(id, 'addAlias: id')) { return; } return await this.addAliases({alias: id}); @@ -116,7 +115,7 @@ class OneSignalUser { Future addTagWithKey(String key, dynamic value) async { if (isMissing(key, 'addTag: key')) return; if (value == null) { - debugPrint('OneSignal: addTag: value is required'); + debugPrint('[OneSignal] addTag: value is required'); return; } return await this.addTags({key: value.toString()}); @@ -131,7 +130,7 @@ class OneSignalUser { for (final key in tags.keys) { if (isMissing(key, 'addTags: key')) return; if (tags[key] == null) { - debugPrint('OneSignal: addTags: value is required'); + debugPrint('[OneSignal] addTags: value is required'); return; } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ddef5b0a..b9629e20 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; bool isMissing(Object? value, String api) { if (value is String && value.isNotEmpty) return false; - debugPrint('OneSignal: $api is required'); + debugPrint('[OneSignal] $api is required'); return true; } diff --git a/test/liveactivities_test.dart b/test/liveactivities_test.dart index 3905b99c..e15cc2b7 100644 --- a/test/liveactivities_test.dart +++ b/test/liveactivities_test.dart @@ -35,6 +35,13 @@ void main() { expect(channelController.state.liveActivityId, activityId); expect(channelController.state.liveActivityToken, token); }); + + test('rejects empty activityId or token', () async { + await liveActivities.enterLiveActivity('', token); + await liveActivities.enterLiveActivity(activityId, ''); + + expect(channelController.state.liveActivityEntered, isNull); + }); }); group('exitLiveActivity', () { @@ -45,6 +52,13 @@ void main() { expect(channelController.state.liveActivityExited, true); expect(channelController.state.liveActivityId, activityId); }); + + test('rejects empty activityId', () async { + // ignore: deprecated_member_use_from_same_package + await liveActivities.exitLiveActivity(''); + + expect(channelController.state.liveActivityExited, isNull); + }); }); group('setupDefault', () { @@ -113,6 +127,19 @@ void main() { expect(channelController.state.liveActivityContent['data']['status'], 'running'); }); + + test('rejects empty activityId', () async { + await liveActivities.startDefault('', {}, {}); + + expect(channelController.state.liveActivityStarted, isNull); + }); + + test('rejects non-map attributes or content', () async { + await liveActivities.startDefault(activityId, null, {}); + await liveActivities.startDefault(activityId, {}, 'content'); + + expect(channelController.state.liveActivityStarted, isNull); + }); }); group('setPushToStartToken', () { @@ -124,6 +151,13 @@ void main() { expect(channelController.state.liveActivityType, activityType); expect(channelController.state.liveActivityPushToken, token); }); + + test('rejects empty activityType or token', () async { + await liveActivities.setPushToStartToken('', token); + await liveActivities.setPushToStartToken(activityType, ''); + + expect(channelController.state.liveActivityPushToStartSet, isNull); + }); }); group('removePushToStartToken', () { @@ -134,6 +168,12 @@ void main() { expect(channelController.state.liveActivityPushToStartRemoved, true); expect(channelController.state.liveActivityType, activityType); }); + + test('rejects empty activityType', () async { + await liveActivities.removePushToStartToken(''); + + expect(channelController.state.liveActivityPushToStartRemoved, isNull); + }); }); }); diff --git a/test/notifications_test.dart b/test/notifications_test.dart index db40fce9..b6d088e0 100644 --- a/test/notifications_test.dart +++ b/test/notifications_test.dart @@ -111,6 +111,12 @@ void main() { expect(channelController.state.removedNotificationGroup, notificationGroup); }); + + test('rejects empty notificationGroup', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await notifications.removeGroupedNotifications(''); + expect(channelController.state.removedNotificationGroup, isNull); + }); }); group('clearAll', () { diff --git a/test/session_test.dart b/test/session_test.dart index 4ef608cc..c3df14fd 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -34,10 +34,10 @@ void main() { expect(channelController.state.addOutcomeCallCount, 3); }); - test('handles empty string outcome name', () async { + test('rejects empty outcome name', () async { await session.addOutcome(''); - expect(channelController.state.addedOutcome, ''); + expect(channelController.state.addOutcomeCallCount, 0); }); }); @@ -58,10 +58,10 @@ void main() { expect(channelController.state.addUniqueOutcomeCallCount, 2); }); - test('handles empty string outcome name', () async { + test('rejects empty outcome name', () async { await session.addUniqueOutcome(''); - expect(channelController.state.addedUniqueOutcome, ''); + expect(channelController.state.addUniqueOutcomeCallCount, 0); }); }); @@ -99,14 +99,10 @@ void main() { expect(channelController.state.addOutcomeWithValueCallCount, 3); }); - test('handles empty string outcome name with value', () async { - const outcomeValue = 15.5; + test('rejects empty outcome name', () async { + await session.addOutcomeWithValue('', 15.5); - await session.addOutcomeWithValue('', outcomeValue); - - expect(channelController.state.addedOutcomeWithValueName, ''); - expect( - channelController.state.addedOutcomeWithValueValue, outcomeValue); + expect(channelController.state.addOutcomeWithValueCallCount, 0); }); }); }); From 0ee670a615d28031075d9761a845a454b9aedefd Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 2 Oct 2026 22:37:16 -0700 Subject: [PATCH 5/6] refactor: unify error logging via logError helper --- lib/src/liveactivities.dart | 2 +- lib/src/user.dart | 5 ++--- lib/src/utils.dart | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/liveactivities.dart b/lib/src/liveactivities.dart index a25c46e5..7791df22 100644 --- a/lib/src/liveactivities.dart +++ b/lib/src/liveactivities.dart @@ -116,7 +116,7 @@ class OneSignalLiveActivities { bool _isMap(Object? value, String api) { if (value is Map) return true; - debugPrint('[OneSignal] $api must be a map'); + logError('$api must be a map'); return false; } } diff --git a/lib/src/user.dart b/lib/src/user.dart index 26dfa978..3fef9665 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/src/pushsubscription.dart'; import 'package:onesignal_flutter/src/utils.dart'; @@ -115,7 +114,7 @@ class OneSignalUser { Future addTagWithKey(String key, dynamic value) async { if (isMissing(key, 'addTag: key')) return; if (value == null) { - debugPrint('[OneSignal] addTag: value is required'); + logError('addTag: value is required'); return; } return await this.addTags({key: value.toString()}); @@ -130,7 +129,7 @@ class OneSignalUser { for (final key in tags.keys) { if (isMissing(key, 'addTags: key')) return; if (tags[key] == null) { - debugPrint('[OneSignal] addTags: value is required'); + logError('addTags: value is required'); return; } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b9629e20..00d56a34 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -2,9 +2,11 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +void logError(String message) => debugPrint('[OneSignal] $message'); + bool isMissing(Object? value, String api) { if (value is String && value.isNotEmpty) return false; - debugPrint('[OneSignal] $api is required'); + logError('$api is required'); return true; } From 22e468c11d8859bab9624fc44f147083a9ce59b6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 2 Oct 2026 22:46:38 -0700 Subject: [PATCH 6/6] fix(session): reject non-finite outcome values --- lib/src/session.dart | 4 ++++ test/session_test.dart | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/src/session.dart b/lib/src/session.dart index 596e2f9b..511edc45 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -24,6 +24,10 @@ class OneSignalSession { /// Counted each time sent successfully, failed ones will be cached and reattempted in future Future addOutcomeWithValue(String name, double value) async { if (isMissing(name, 'addOutcomeWithValue: name')) return; + if (!value.isFinite) { + logError('addOutcomeWithValue: value must be a finite number'); + return; + } return await _channel.invokeMethod("OneSignal#addOutcomeWithValue", {"outcome_name": name, "outcome_value": value}); } diff --git a/test/session_test.dart b/test/session_test.dart index c3df14fd..40889c78 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -104,6 +104,14 @@ void main() { expect(channelController.state.addOutcomeWithValueCallCount, 0); }); + + test('rejects non-finite value', () async { + await session.addOutcomeWithValue('outcome', double.nan); + await session.addOutcomeWithValue('outcome', double.infinity); + await session.addOutcomeWithValue('outcome', double.negativeInfinity); + + expect(channelController.state.addOutcomeWithValueCallCount, 0); + }); }); }); }