diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
index 964ac084..2f6954ff 100644
--- a/MIGRATION_GUIDE.md
+++ b/MIGRATION_GUIDE.md
@@ -209,6 +209,9 @@ The User name space is accessible via `OneSignal.User` and provides access to us
| **Flutter** | **Description** |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OneSignal.User.setLanguage("en");` | *Set the 2-character language for this user.* |
+| `OneSignal.User.addObserver(OnUserChangeObserver observer);`
**_See below for usage_** | *Add a User State observer which contains the nullable onesignalId and externalId. The listener will be fired when these values change.* |
+| `await OneSignal.User.getOnesignalId();` | *Returns the nullable OneSignal ID for the current user.* |
+| `await OneSignal.User.getExternalId();` | *Returns the nullable External ID for the current user.* |
| `OneSignal.User.addAlias("ALIAS_LABEL", "ALIAS_ID");` | *Set an alias for the current user. If this alias label already exists on this user, it will be overwritten with the new alias id.* |
| `OneSignal.User.addAliases({ALIAS_LABEL_01: "ALIAS_ID_01", ALIAS_LABEL_02: "ALIAS_ID_02"});` | *Set aliases for the current user. If any alias already exists, it will be overwritten to the new values.* |
| `OneSignal.User.removeAlias("ALIAS_LABEL");` | *Remove an alias from the current user.* |
@@ -223,6 +226,20 @@ The User name space is accessible via `OneSignal.User` and provides access to us
| `OneSignal.User.removeTags(["KEY_01", "KEY_02"]);` | *Remove multiple tags with the provided keys from the current user.* |
| `OneSignal.User.getTags();` | *Returns the local tags for the current user.* |
+### User State Observer
+
+The `OnUserChangeObserver` will be fired when the user changes. This method's parameter is the current `UserChangedState` which includes the current state.
+
+```dart
+OneSignal.User.addObserver((state) {
+ var userState = state.jsonRepresentation();
+ print('OneSignal user changed: $userState');
+});
+
+/// Remove a user state observer that has been previously added.
+OneSignal.User.removeObserver(observer);
+```
+
## Push Subscription Namespace
diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java b/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java
index d486842e..69831527 100644
--- a/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java
+++ b/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java
@@ -2,6 +2,8 @@
import android.util.Log;
+import com.onesignal.user.state.UserChangedState;
+import com.onesignal.user.state.UserState;
import com.onesignal.user.subscriptions.ISubscription;
import com.onesignal.user.subscriptions.IPushSubscription;
import com.onesignal.user.subscriptions.PushSubscriptionChangedState;
@@ -187,6 +189,18 @@ static HashMap convertPushSubscriptionState(PushSubscriptionStat
return hash;
}
+ static HashMap convertUserState(UserState state) throws JSONException {
+ HashMap hash = new HashMap<>();
+
+ String onesignalId = setNullIfEmpty(state.getOnesignalId());
+ String externalId = setNullIfEmpty(state.getExternalId());
+
+ hash.put("onesignalId", onesignalId);
+ hash.put("externalId", externalId);
+
+ return hash;
+ }
+
static HashMap convertOnPushSubscriptionChange(PushSubscriptionChangedState changedState) throws JSONException {
HashMap hash = new HashMap<>();
@@ -197,6 +211,14 @@ static HashMap convertOnPushSubscriptionChange(PushSubscriptionC
return hash;
}
+ static HashMap convertOnUserStateChange(UserChangedState changedState) throws JSONException {
+ HashMap hash = new HashMap<>();
+
+
+ hash.put("current", convertUserState(changedState.getCurrent()));
+
+ return hash;
+ }
static HashMap convertJSONObjectToHashMap(JSONObject object) throws JSONException {
HashMap hash = new HashMap<>();
@@ -242,4 +264,9 @@ else if (val instanceof JSONObject)
return list;
}
+
+ /** Helper method to return null value if string is empty **/
+ static String setNullIfEmpty(String value) {
+ return value.isEmpty() ? null : value;
+ }
}
diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalUser.java b/android/src/main/java/com/onesignal/flutter/OneSignalUser.java
index accd29cc..b14c0087 100644
--- a/android/src/main/java/com/onesignal/flutter/OneSignalUser.java
+++ b/android/src/main/java/com/onesignal/flutter/OneSignalUser.java
@@ -2,6 +2,9 @@
import com.onesignal.OneSignal;
import com.onesignal.debug.LogLevel;
+import com.onesignal.debug.internal.logging.Logging;
+import com.onesignal.user.state.IUserStateObserver;
+import com.onesignal.user.state.UserChangedState;
import org.json.JSONException;
import org.json.JSONObject;
@@ -18,8 +21,7 @@
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.Registrar;
-public class OneSignalUser extends FlutterRegistrarResponder implements MethodCallHandler {
- private MethodChannel channel;
+public class OneSignalUser extends FlutterRegistrarResponder implements MethodCallHandler, IUserStateObserver {
static void registerWith(BinaryMessenger messenger) {
OneSignalUser controller = new OneSignalUser();
@@ -32,6 +34,10 @@ static void registerWith(BinaryMessenger messenger) {
public void onMethodCall(MethodCall call, Result result) {
if (call.method.contentEquals("OneSignal#setLanguage"))
this.setLanguage(call, result);
+ else if (call.method.contentEquals("OneSignal#getOnesignalId"))
+ this.getOnesignalId(call, result);
+ else if (call.method.contentEquals("OneSignal#getExternalId"))
+ this.getExternalId(call, result);
else if (call.method.contentEquals("OneSignal#addAliases"))
this.addAliases(call, result);
else if (call.method.contentEquals("OneSignal#removeAliases"))
@@ -50,6 +56,8 @@ else if (call.method.contentEquals("OneSignal#removeTags"))
this.removeTags(call, result);
else if (call.method.contentEquals("OneSignal#getTags"))
this.getTags(call, result);
+ else if (call.method.contentEquals("OneSignal#lifecycleInit"))
+ this.lifecycleInit();
else
replyNotImplemented(result);
}
@@ -63,6 +71,26 @@ private void setLanguage(MethodCall call, Result result) {
replySuccess(result, null);
}
+ private void lifecycleInit() {
+ OneSignal.getUser().addObserver(this);
+ }
+
+ private void getOnesignalId(MethodCall call, Result result) {
+ String onesignalId = OneSignal.getUser().getOnesignalId();
+ if (onesignalId.isEmpty()) {
+ onesignalId = null;
+ }
+ replySuccess(result, onesignalId);
+ }
+
+ private void getExternalId(MethodCall call, Result result) {
+ String externalId = OneSignal.getUser().getExternalId();
+ if (externalId.isEmpty()) {
+ externalId = null;
+ }
+ replySuccess(result, externalId);
+ }
+
private void addAliases(MethodCall call, Result result) {
// call.arguments is being casted to a Map so a try-catch with
// a ClassCastException will be thrown
@@ -130,4 +158,14 @@ private void removeTags(MethodCall call, Result result) {
private void getTags(MethodCall call, Result result) {
replySuccess(result, OneSignal.getUser().getTags());
}
+
+ @Override
+ public void onUserStateChange(UserChangedState userChangedState) {
+ try {
+ invokeMethodOnUiThread("OneSignal#onUserStateChange", OneSignalSerializer.convertOnUserStateChange(userChangedState));
+ } catch (JSONException e) {
+ e.getStackTrace();
+ Logging.error("Encountered an error attempting to convert UserChangedState object to hash map:" + e.toString(), null);
+ }
+ }
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 65cbb785..1016ad3d 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -53,6 +53,11 @@ class _MyAppState extends State {
print(state.current.jsonRepresentation());
});
+ OneSignal.User.addObserver((state) {
+ var userState = state.jsonRepresentation();
+ print('OneSignal user changed: $userState');
+ });
+
OneSignal.Notifications.addPermissionObserver((state) {
print("Has permission " + state.toString());
});
@@ -193,6 +198,11 @@ class _MyAppState extends State {
OneSignal.Location.setShared(true);
}
+ void _handleGetExternalId() async {
+ var externalId = await OneSignal.User.getExternalId();
+ print('External ID: $externalId');
+ }
+
void _handleLogin() {
print("Setting external user ID");
if (_externalUserId == null) return;
@@ -205,6 +215,11 @@ class _MyAppState extends State {
OneSignal.User.removeAlias("fb_id");
}
+ void _handleGetOnesignalId() async {
+ var onesignalId = await OneSignal.User.getOnesignalId();
+ print('OneSignal ID: $onesignalId');
+ }
+
oneSignalInAppMessagingTriggerExamples() async {
/// Example addTrigger call for IAM
/// This will add 1 trigger so if there are any IAM satisfying it, it
@@ -365,6 +380,10 @@ class _MyAppState extends State {
height: 8.0,
)
]),
+ new TableRow(children: [
+ new OneSignalButton("Get External User ID",
+ _handleGetExternalId, !_enableConsentButton)
+ ]),
new TableRow(children: [
new OneSignalButton("Set External User ID", _handleLogin,
!_enableConsentButton)
@@ -373,6 +392,10 @@ class _MyAppState extends State {
new OneSignalButton("Remove External User ID",
_handleLogout, !_enableConsentButton)
]),
+ new TableRow(children: [
+ new OneSignalButton("Get OneSignal ID",
+ _handleGetOnesignalId, !_enableConsentButton)
+ ]),
new TableRow(children: [
new TextField(
textAlign: TextAlign.center,
diff --git a/ios/Classes/OSFlutterUser.m b/ios/Classes/OSFlutterUser.m
index 4e183136..698e25e1 100644
--- a/ios/Classes/OSFlutterUser.m
+++ b/ios/Classes/OSFlutterUser.m
@@ -47,6 +47,10 @@ + (void)registerWithRegistrar:(NSObject*)registrar {
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"OneSignal#setLanguage" isEqualToString:call.method])
[self setLanguage:call withResult:result];
+ else if ([@"OneSignal#getOnesignalId" isEqualToString:call.method])
+ [self getOnesignalId:call withResult:result];
+ else if ([@"OneSignal#getExternalId" isEqualToString:call.method])
+ [self getExternalId:call withResult:result];
else if ([@"OneSignal#addAliases" isEqualToString:call.method])
[self addAliases:call withResult:result];
else if ([@"OneSignal#removeAliases" isEqualToString:call.method])
@@ -65,7 +69,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
[self addSms:call withResult:result];
else if ([@"OneSignal#removeSms" isEqualToString:call.method])
[self removeSms:call withResult:result];
-
+ else if ([@"OneSignal#lifecycleInit" isEqualToString:call.method])
+ [self lifecycleInit:call withResult:result];
else
result(FlutterMethodNotImplemented);
}
@@ -132,4 +137,41 @@ - (void)removeSms:(FlutterMethodCall *)call withResult:(FlutterResult)result {
result(nil);
}
+- (void)lifecycleInit:(FlutterMethodCall *)call withResult:(FlutterResult)result {
+ [OneSignal.User addObserver:self];
+ result(nil);
+}
+
+- (void)onUserStateDidChangeWithState:(OSUserChangedState *)state {
+ NSString *onesignalId = [self getStringOrNSNull:state.current.onesignalId];
+ NSString *externalId = [self getStringOrNSNull:state.current.externalId];
+
+ NSMutableDictionary *result = [NSMutableDictionary new];
+
+ NSMutableDictionary *currentObject = [NSMutableDictionary new];
+
+ currentObject[@"onesignalId"] = onesignalId;
+ currentObject[@"externalId"] = externalId;
+ result[@"current"] = currentObject;
+
+ [self.channel invokeMethod:@"OneSignal#onUserStateChange" arguments:result];
+}
+
+- (void)getOnesignalId:(FlutterMethodCall *)call withResult:(FlutterResult)result {
+ result(OneSignal.User.onesignalId);
+}
+
+- (void)getExternalId:(FlutterMethodCall *)call withResult:(FlutterResult)result {
+ result(OneSignal.User.externalId);
+}
+
+/** Helper method to return NSNull if string is empty or nil **/
+- (NSString *)getStringOrNSNull:(NSString *)string {
+ // length method can be used on nil and strings
+ if (string.length > 0) {
+ return string;
+ } else {
+ return [NSNull null];
+ }
+}
@end
diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart
index e3938037..d9ad1724 100644
--- a/lib/onesignal_flutter.dart
+++ b/lib/onesignal_flutter.dart
@@ -48,6 +48,7 @@ class OneSignal {
static void initialize(String appId) {
_channel.invokeMethod('OneSignal#initialize', {'appId': appId});
InAppMessages.lifecycleInit();
+ User.lifecycleInit();
User.pushSubscription.lifecycleInit();
Notifications.lifecycleInit();
}
diff --git a/lib/src/user.dart b/lib/src/user.dart
index 4f040d2c..9889bc92 100644
--- a/lib/src/user.dart
+++ b/lib/src/user.dart
@@ -1,8 +1,45 @@
import 'dart:async';
import 'package:flutter/services.dart';
+import 'package:onesignal_flutter/src/utils.dart';
import 'package:onesignal_flutter/src/pushsubscription.dart';
+typedef void OnUserChangeObserver(OSUserChangedState stateChanges);
+
+/// Represents the current user state with OneSignal.
+class OSUserState extends JSONStringRepresentable {
+ String? onesignalId;
+ String? externalId;
+
+ OSUserState(Map json) {
+ if (json.containsKey('onesignalId'))
+ this.onesignalId = json['onesignalId'] as String?;
+ if (json.containsKey('externalId'))
+ this.externalId = json['externalId'] as String?;
+ }
+
+ String jsonRepresentation() {
+ return convertToJsonString(
+ {'onesignalId': this.onesignalId, 'externalId': this.externalId});
+ }
+}
+
+/// An instance of this class describes a change in the user state.
+class OSUserChangedState extends JSONStringRepresentable {
+ late OSUserState current;
+
+ OSUserChangedState(Map json) {
+ if (json.containsKey('current'))
+ this.current = OSUserState(json['current'].cast());
+ }
+
+ String jsonRepresentation() {
+ return convertToJsonString({
+ 'current': current.jsonRepresentation(),
+ });
+ }
+}
+
class OneSignalUser {
static OneSignalPushSubscription _pushSubscription =
new OneSignalPushSubscription();
@@ -12,6 +49,12 @@ class OneSignalUser {
// private channels used to bridge to ObjC/Java
MethodChannel _channel = const MethodChannel('OneSignal#user');
+ List _observers = [];
+ // constructor method
+ OneSignalUser() {
+ this._channel.setMethodCallHandler(_handleMethod);
+ }
+
/// Sets the user's language.
///
/// Sets the user's language to [language] this also applies to
@@ -111,4 +154,45 @@ class OneSignalUser {
Future removeSms(String smsNumber) async {
return await _channel.invokeMethod("OneSignal#removeSms", smsNumber);
}
+
+ /// Returns the nullable External ID for the current user.
+ Future getExternalId() async {
+ return await _channel.invokeMethod("OneSignal#getExternalId");
+ }
+
+ /// Returns the nullable OneSignal ID for the current user.
+ Future getOnesignalId() async {
+ return await _channel.invokeMethod("OneSignal#getOnesignalId");
+ }
+
+ /// Add an observer that fires when the OneSignal User state changes.
+ /// *Important* When using the observer to retrieve the onesignalId, check the
+ /// externalId as well to confirm the values are associated with the expected user.*
+ void addObserver(OnUserChangeObserver observer) {
+ _observers.add(observer);
+ }
+
+ // Remove a user state observer that has been previously added.
+ void removeObserver(OnUserChangeObserver observer) {
+ _observers.remove(observer);
+ }
+
+ Future lifecycleInit() async {
+ return await _channel.invokeMethod("OneSignal#lifecycleInit");
+ }
+
+ // Private function that gets called by ObjC/Java
+ Future _handleMethod(MethodCall call) async {
+ if (call.method == 'OneSignal#onUserStateChange') {
+ this._onUserStateChange(
+ OSUserChangedState(call.arguments.cast()));
+ }
+ return null;
+ }
+
+ void _onUserStateChange(OSUserChangedState stateChanges) async {
+ for (var observer in _observers) {
+ observer(stateChanges);
+ }
+ }
}