Skip to content

Commit

Permalink
Merge pull request #856 from OneSignal/feat/add_get_ids
Browse files Browse the repository at this point in the history
Add user state observer and ID getters
  • Loading branch information
jennantilla authored Mar 21, 2024
2 parents 4d6e527 + 6bd8b66 commit cbb34c4
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 3 deletions.
17 changes: 17 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);` <br><br>**_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.* |
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -187,6 +189,18 @@ static HashMap<String, Object> convertPushSubscriptionState(PushSubscriptionStat
return hash;
}

static HashMap<String, Object> convertUserState(UserState state) throws JSONException {
HashMap<String, Object> 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<String, Object> convertOnPushSubscriptionChange(PushSubscriptionChangedState changedState) throws JSONException {
HashMap<String, Object> hash = new HashMap<>();

Expand All @@ -197,6 +211,14 @@ static HashMap<String, Object> convertOnPushSubscriptionChange(PushSubscriptionC
return hash;
}

static HashMap<String, Object> convertOnUserStateChange(UserChangedState changedState) throws JSONException {
HashMap<String, Object> hash = new HashMap<>();


hash.put("current", convertUserState(changedState.getCurrent()));

return hash;
}

static HashMap<String, Object> convertJSONObjectToHashMap(JSONObject object) throws JSONException {
HashMap<String, Object> hash = new HashMap<>();
Expand Down Expand Up @@ -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;
}
}
42 changes: 40 additions & 2 deletions android/src/main/java/com/onesignal/flutter/OneSignalUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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"))
Expand All @@ -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);
}
Expand All @@ -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<String, Object> so a try-catch with
// a ClassCastException will be thrown
Expand Down Expand Up @@ -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);
}
}
}
23 changes: 23 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class _MyAppState extends State<MyApp> {
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());
});
Expand Down Expand Up @@ -193,6 +198,11 @@ class _MyAppState extends State<MyApp> {
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;
Expand All @@ -205,6 +215,11 @@ class _MyAppState extends State<MyApp> {
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
Expand Down Expand Up @@ -365,6 +380,10 @@ class _MyAppState extends State<MyApp> {
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)
Expand All @@ -373,6 +392,10 @@ class _MyAppState extends State<MyApp> {
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,
Expand Down
44 changes: 43 additions & 1 deletion ios/Classes/OSFlutterUser.m
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)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])
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/onesignal_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
84 changes: 84 additions & 0 deletions lib/src/user.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> 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<String, dynamic> json) {
if (json.containsKey('current'))
this.current = OSUserState(json['current'].cast<String, dynamic>());
}

String jsonRepresentation() {
return convertToJsonString(<String, dynamic>{
'current': current.jsonRepresentation(),
});
}
}

class OneSignalUser {
static OneSignalPushSubscription _pushSubscription =
new OneSignalPushSubscription();
Expand All @@ -12,6 +49,12 @@ class OneSignalUser {
// private channels used to bridge to ObjC/Java
MethodChannel _channel = const MethodChannel('OneSignal#user');

List<OnUserChangeObserver> _observers = <OnUserChangeObserver>[];
// constructor method
OneSignalUser() {
this._channel.setMethodCallHandler(_handleMethod);
}

/// Sets the user's language.
///
/// Sets the user's language to [language] this also applies to
Expand Down Expand Up @@ -111,4 +154,45 @@ class OneSignalUser {
Future<void> removeSms(String smsNumber) async {
return await _channel.invokeMethod("OneSignal#removeSms", smsNumber);
}

/// Returns the nullable External ID for the current user.
Future<String?> getExternalId() async {
return await _channel.invokeMethod("OneSignal#getExternalId");
}

/// Returns the nullable OneSignal ID for the current user.
Future<String?> 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<void> lifecycleInit() async {
return await _channel.invokeMethod("OneSignal#lifecycleInit");
}

// Private function that gets called by ObjC/Java
Future<Null> _handleMethod(MethodCall call) async {
if (call.method == 'OneSignal#onUserStateChange') {
this._onUserStateChange(
OSUserChangedState(call.arguments.cast<String, dynamic>()));
}
return null;
}

void _onUserStateChange(OSUserChangedState stateChanges) async {
for (var observer in _observers) {
observer(stateChanges);
}
}
}

0 comments on commit cbb34c4

Please sign in to comment.