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); + } + } }