diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index 922ea569..974140db 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 (isMissing(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 (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 ca10b3e9..638cce43 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 (isMissing(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 (isMissing(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 (isMissing(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 (isMissing(key, 'removeTriggers: key')) return; + } return await _channel.invokeMethod("OneSignal#removeTriggers", keys); } diff --git a/lib/src/liveactivities.dart b/lib/src/liveactivities.dart index 819e496c..7791df22 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; + logError('$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..511edc45 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,25 @@ 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; + 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/lib/src/user.dart b/lib/src/user.dart index cc8dfa43..3fef9665 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -62,6 +62,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 { + // Empty string is the reset to the device language. return await _channel.invokeMethod("OneSignal#setLanguage", { 'language': language, }); @@ -72,6 +73,9 @@ 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')) { + return; + } return await this.addAliases({alias: id}); } @@ -79,16 +83,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 (isMissing(label, 'addAliases: label') || + isMissing(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 (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 (isMissing(label, 'removeAliases: label')) return; + } return await _channel.invokeMethod("OneSignal#removeAliases", aliases); } @@ -98,6 +112,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 (isMissing(key, 'addTag: key')) return; + if (value == null) { + logError('addTag: value is required'); + return; + } return await this.addTags({key: value.toString()}); } @@ -107,6 +126,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 (isMissing(key, 'addTags: key')) return; + if (tags[key] == null) { + logError('addTags: value is required'); + return; + } + } tags.forEach((key, value) { tags[key] = value.toString(); }); @@ -115,11 +141,15 @@ class OneSignalUser { /// Remove the data tag with the provided [key] from the current user. Future removeTag(String key) async { + 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 (isMissing(key, 'removeTags: key')) return; + } return await _channel.invokeMethod("OneSignal#removeTags", tags); } @@ -133,6 +163,7 @@ class OneSignalUser { /// Add a new [email] subscription to the current user. Future addEmail(String email) async { + if (isMissing(email, 'addEmail: email')) return; return await _channel.invokeMethod("OneSignal#addEmail", email); } @@ -141,6 +172,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 (isMissing(email, 'removeEmail: email')) return; return await _channel.invokeMethod("OneSignal#removeEmail", email); } @@ -148,6 +180,7 @@ class OneSignalUser { /// /// Add an SMS subscription by adding an [smsNumber] Future addSms(String smsNumber) async { + if (isMissing(smsNumber, 'addSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#addSms", smsNumber); } @@ -156,6 +189,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 (isMissing(smsNumber, 'removeSms: smsNumber')) return; return await _channel.invokeMethod("OneSignal#removeSms", smsNumber); } @@ -175,6 +209,7 @@ class OneSignalUser { /// [properties] is an optional map of custom properties associated with the event. Future trackEvent(String name, [Map? properties]) async { + 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 ef291b54..00d56a34 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,5 +1,15 @@ 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; + logError('$api is required'); + return true; +} + /// An abstract class to provide JSON decoding abstract class JSONStringRepresentable { String jsonRepresentation(); 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/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); + }); + }); } diff --git a/test/session_test.dart b/test/session_test.dart index 4ef608cc..40889c78 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,18 @@ 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.addOutcomeWithValueCallCount, 0); + }); - expect(channelController.state.addedOutcomeWithValueName, ''); - expect( - channelController.state.addedOutcomeWithValueValue, outcomeValue); + 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); }); }); });