diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index cecd41b5..41136a4b 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -212,6 +212,22 @@ The User namespace is accessible via `OneSignal.User` and provides access to use | `OneSignal.User.removeTag("KEY")` | _Remove the data tag with the provided key from the current user._ | | `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._| +| `OneSignal.User.addEventListener("change", (event: UserChangedState) => void)`

**_See below for usage_** | _Add a User State callback which contains the nullable onesignalId and externalId. The listener will be fired when these values change._| +| `await OneSignal.User.getOnesignalId()` | _Returns the OneSignal ID for the current user, which can be null if it is not yet available._| +| `await OneSignal.User.getExternalId()` | _Returns the External ID for the current user, which can be null if not set._| + +### User State Listener + +```typescript + const listener = (event: UserChangedState) => { + console.log("User changed: " + (event)); + }; + + OneSignal.User.addEventListener("change", listener); + // Remove the listener + OneSignal.User.removeEventListener("change", listener); +``` + ## Push Subscription Namespace The Push Subscription namespace is accessible via `OneSignal.User.pushSubscription` and provides access to push subscription-scoped functionality. diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index debabf3e..5c9606f1 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -74,6 +74,9 @@ of this software and associated documentation files (the "Software"), to deal import com.onesignal.user.subscriptions.IPushSubscriptionObserver; import com.onesignal.user.subscriptions.PushSubscriptionState; import com.onesignal.user.subscriptions.PushSubscriptionChangedState; +import com.onesignal.user.state.UserState; +import com.onesignal.user.state.UserChangedState; +import com.onesignal.user.state.IUserStateObserver; import org.json.JSONException; import java.util.HashMap; @@ -82,6 +85,7 @@ of this software and associated documentation files (the "Software"), to deal public class RNOneSignal extends ReactContextBaseJavaModule implements IPushSubscriptionObserver, IPermissionObserver, + IUserStateObserver, LifecycleEventListener, INotificationLifecycleListener{ private ReactApplicationContext mReactApplicationContext; @@ -90,6 +94,7 @@ public class RNOneSignal extends ReactContextBaseJavaModule implements private boolean oneSignalInitDone; private boolean hasSetPermissionObserver = false; private boolean hasSetPushSubscriptionObserver = false; + private boolean hasSetUserStateObserver = false; private HashMap notificationWillDisplayCache; private HashMap preventDefaultCache; @@ -162,6 +167,7 @@ public void onClick(INotificationClickEvent event) { private void removeObservers() { this.removePermissionObserver(); this.removePushSubscriptionObserver(); + this.removeUserStateObserver(); } private void removeHandlers() { @@ -676,6 +682,53 @@ public void removeAliases(ReadableArray aliasLabels) { OneSignal.getUser().removeAliases(RNUtils.convertReadableArrayIntoStringCollection(aliasLabels)); } + @ReactMethod + public void getOnesignalId(Promise promise) { + String onesignalId = OneSignal.getUser().getOnesignalId(); + if (onesignalId.isEmpty()) { + onesignalId = null; + } + promise.resolve(onesignalId); + + } + + @ReactMethod + public void getExternalId(Promise promise) { + String externalId = OneSignal.getUser().getExternalId(); + if (externalId.isEmpty()) { + externalId = null; + } + promise.resolve(externalId); + } + + @ReactMethod + public void addUserStateObserver() { + if (!hasSetUserStateObserver) { + OneSignal.getUser().addObserver(this); + hasSetUserStateObserver = true; + } + } + + @Override + public void onUserStateChange(UserChangedState state) { + try { + sendEvent("OneSignal-userStateChanged", + RNUtils.convertHashMapToWritableMap( + RNUtils.convertUserChangedStateToMap(state))); + Log.i("OneSignal", "sending user state change event"); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @ReactMethod + public void removeUserStateObserver() { + if (hasSetUserStateObserver) { + OneSignal.getUser().removeObserver(this); + hasSetUserStateObserver = false; + } + } + /** Added for NativeEventEmitter */ @ReactMethod public void addListener(String eventName) { diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index a9b034a9..e9989b76 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -27,6 +27,8 @@ import com.onesignal.user.subscriptions.IPushSubscription; import com.onesignal.user.subscriptions.PushSubscriptionState; import com.onesignal.user.subscriptions.PushSubscriptionChangedState; +import com.onesignal.user.state.UserState; +import com.onesignal.user.state.UserChangedState; import org.json.JSONArray; import org.json.JSONException; @@ -197,6 +199,25 @@ public static HashMap convertPushSubscriptionStateToMap(PushSubs return hash; } + public static HashMap convertUserStateToMap(UserState user) { + HashMap hash = new HashMap<>(); + + if (user.getExternalId() != null && !user.getExternalId().isEmpty()) { + hash.put("externalId", user.getExternalId()); + } + else { + hash.put("externalId", JSONObject.NULL); + } + if (user.getOnesignalId() != null && !user.getOnesignalId().isEmpty()) { + hash.put("onesignalId", user.getOnesignalId()); + } + else { + hash.put("onesignalId", JSONObject.NULL); + } + + return hash; + } + public static HashMap convertPushSubscriptionChangedStateToMap(PushSubscriptionChangedState state) { HashMap hash = new HashMap<>(); hash.put("current", convertPushSubscriptionStateToMap(state.getCurrent())); @@ -205,6 +226,13 @@ public static HashMap convertPushSubscriptionChangedStateToMap(P return hash; } + public static HashMap convertUserChangedStateToMap(UserChangedState state) { + HashMap hash = new HashMap<>(); + hash.put("current", convertUserStateToMap(state.getCurrent())); + + return hash; + } + public static HashMap convertJSONObjectToHashMap(JSONObject object) throws JSONException { HashMap hash = new HashMap<>(); diff --git a/examples/RNOneSignalTS/src/OSButtons.tsx b/examples/RNOneSignalTS/src/OSButtons.tsx index ce3ebfc4..5d6d3b48 100644 --- a/examples/RNOneSignalTS/src/OSButtons.tsx +++ b/examples/RNOneSignalTS/src/OSButtons.tsx @@ -277,8 +277,8 @@ class OSButtons extends React.Component { }); const getTagsButton = renderButtonView('Get tags', async () => { - const tags = await OneSignal.User.getTags(); - loggingFunction('Tags:', tags); + const tags = await OneSignal.User.getTags(); + loggingFunction('Tags:', tags); }); const setLanguageButton = renderButtonView('Set Language', () => { @@ -342,6 +342,22 @@ class OSButtons extends React.Component { }, ); + const getOnesignalIdButton = renderButtonView( + 'Get OneSignal Id', + async () => { + const onesignalId = await OneSignal.User.getOnesignalId(); + loggingFunction('OneSignal Id: ', onesignalId); + }, + ); + + const getExternalIdButton = renderButtonView( + 'Get External Id', + async () => { + const externalId = await OneSignal.User.getExternalId(); + loggingFunction('External Id:', externalId); + }, + ); + return [ loginButton, logoutButton, @@ -359,6 +375,8 @@ class OSButtons extends React.Component { removeAliasButton, addAliasesButton, removeAliasesButton, + getOnesignalIdButton, + getExternalIdButton, ]; } diff --git a/examples/RNOneSignalTS/src/OSDemo.tsx b/examples/RNOneSignalTS/src/OSDemo.tsx index 825e3ab0..94f2dbf1 100644 --- a/examples/RNOneSignalTS/src/OSDemo.tsx +++ b/examples/RNOneSignalTS/src/OSDemo.tsx @@ -99,6 +99,10 @@ class OSDemo extends React.Component { OneSignal.Notifications.addEventListener('permissionChange', (granted) => { this.OSLog('OneSignal: permission changed:', granted); }); + + OneSignal.User.addEventListener('change', (event) => { + this.OSLog('OneSignal: user changed: ', event); + }); } OSLog = (message: string, optionalArg: any = null) => { diff --git a/ios/RCTOneSignal/RCTOneSignal.h b/ios/RCTOneSignal/RCTOneSignal.h index ccf3336e..be93f668 100644 --- a/ios/RCTOneSignal/RCTOneSignal.h +++ b/ios/RCTOneSignal/RCTOneSignal.h @@ -5,8 +5,7 @@ #import "../OneSignalFramework.h" #endif -@interface RCTOneSignal : NSObject - +@interface RCTOneSignal : NSObject + (RCTOneSignal *) sharedInstance; @end diff --git a/ios/RCTOneSignal/RCTOneSignal.m b/ios/RCTOneSignal/RCTOneSignal.m index 85118514..eb451540 100644 --- a/ios/RCTOneSignal/RCTOneSignal.m +++ b/ios/RCTOneSignal/RCTOneSignal.m @@ -77,6 +77,31 @@ - (void)sendEvent:(NSString *)eventName withBody:(NSDictionary *)body { [RCTOneSignalEventEmitter sendEventWithName:eventName withBody:body]; } +- (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state { + NSString *onesignalId = state.current.onesignalId; + NSString *externalId = state.current.externalId; + + NSMutableDictionary *currentDictionary = [NSMutableDictionary dictionary]; + + if (onesignalId.length > 0) { + [currentDictionary setObject:onesignalId forKey:@"onesignalId"]; + } + else { + [currentDictionary setObject:[NSNull null] forKey:@"onesignalId"]; + } + + if (externalId.length > 0) { + [currentDictionary setObject:externalId forKey:@"externalId"]; + } + else { + [currentDictionary setObject:[NSNull null] forKey:@"externalId"]; + } + + NSDictionary *result = @{@"current": currentDictionary}; + + [self sendEvent:OSEventString(UserStateChanged) withBody:result]; +} + - (void)onPushSubscriptionDidChangeWithState:(OSPushSubscriptionChangedState * _Nonnull)state { NSMutableDictionary *result = [NSMutableDictionary new]; diff --git a/ios/RCTOneSignal/RCTOneSignalEventEmitter.h b/ios/RCTOneSignal/RCTOneSignalEventEmitter.h index 458446e8..ca153320 100644 --- a/ios/RCTOneSignal/RCTOneSignalEventEmitter.h +++ b/ios/RCTOneSignal/RCTOneSignalEventEmitter.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OSNotificationEventTypes) { PermissionChanged, SubscriptionChanged, + UserStateChanged, NotificationWillDisplayInForeground, NotificationClicked, InAppMessageClicked, @@ -24,7 +25,7 @@ typedef NS_ENUM(NSInteger, OSNotificationEventTypes) { InAppMessageDidDismiss, }; -#define OSNotificationEventTypesArray @[@"OneSignal-permissionChanged",@"OneSignal-subscriptionChanged",@"OneSignal-notificationWillDisplayInForeground",@"OneSignal-notificationClicked",@"OneSignal-inAppMessageClicked", @"OneSignal-inAppMessageWillDisplay", @"OneSignal-inAppMessageDidDisplay", @"OneSignal-inAppMessageWillDismiss", @"OneSignal-inAppMessageDidDismiss"] +#define OSNotificationEventTypesArray @[@"OneSignal-permissionChanged",@"OneSignal-subscriptionChanged",@"OneSignal-userStateChanged",@"OneSignal-notificationWillDisplayInForeground",@"OneSignal-notificationClicked",@"OneSignal-inAppMessageClicked", @"OneSignal-inAppMessageWillDisplay", @"OneSignal-inAppMessageDidDisplay", @"OneSignal-inAppMessageWillDismiss", @"OneSignal-inAppMessageDidDismiss"] #define OSEventString(enum) [OSNotificationEventTypesArray objectAtIndex:enum] diff --git a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m index 0d72549c..03b15994 100644 --- a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m +++ b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m @@ -9,6 +9,7 @@ @implementation RCTOneSignalEventEmitter { BOOL _hasListeners; BOOL _hasSetSubscriptionObserver; BOOL _hasSetPermissionObserver; + BOOL _hasSetUserStateObserver; BOOL _hasAddedNotificationClickListener; BOOL _hasAddedNotificationForegroundLifecycleListener; BOOL _hasAddedInAppMessageClickListener; @@ -310,6 +311,13 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { } // OneSignal.User namespace methods +RCT_EXPORT_METHOD(addUserStateObserver) { + if (!_hasSetUserStateObserver) { + [OneSignal.User addObserver:[RCTOneSignal sharedInstance]]; + _hasSetUserStateObserver = true; + } +} + RCT_EXPORT_METHOD(addPushSubscriptionObserver) { if (!_hasSetSubscriptionObserver) { [OneSignal.User.pushSubscription addObserver:[RCTOneSignal sharedInstance]]; @@ -365,6 +373,30 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { resolve(tags); } +RCT_REMAP_METHOD(getOnesignalId, + getOnesignalIdResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *onesignalId = OneSignal.User.onesignalId; + + if (onesignalId == nil || [onesignalId length] == 0) { + resolve([NSNull null]); // Resolve with null if nil or empty + } else { + resolve(onesignalId); + } +} + +RCT_REMAP_METHOD(getExternalId, + getExternalIdResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *externalId = OneSignal.User.externalId; + + if (externalId == nil || [externalId length] == 0) { + resolve([NSNull null]); // Resolve with null if nil or empty + } else { + resolve(externalId); + } +} + RCT_EXPORT_METHOD(addAlias:(NSString *)label :(NSString *)id) { [OneSignal.User addAliasWithLabel:label id:id]; } diff --git a/src/events/EventManager.ts b/src/events/EventManager.ts index 2c4eeaa1..fd81128a 100644 --- a/src/events/EventManager.ts +++ b/src/events/EventManager.ts @@ -7,6 +7,7 @@ import NotificationWillDisplayEvent from './NotificationWillDisplayEvent'; import { PERMISSION_CHANGED, SUBSCRIPTION_CHANGED, + USER_STATE_CHANGED, NOTIFICATION_WILL_DISPLAY, NOTIFICATION_CLICKED, IN_APP_MESSAGE_CLICKED, @@ -20,6 +21,7 @@ import OSNotification from '../OSNotification'; const eventList = [ PERMISSION_CHANGED, SUBSCRIPTION_CHANGED, + USER_STATE_CHANGED, NOTIFICATION_WILL_DISPLAY, NOTIFICATION_CLICKED, IN_APP_MESSAGE_CLICKED, diff --git a/src/events/events.ts b/src/events/events.ts index 3ac0c57d..d2e6541e 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -8,3 +8,4 @@ export const IN_APP_MESSAGE_WILL_DISMISS = 'OneSignal-inAppMessageWillDismiss'; export const IN_APP_MESSAGE_DID_DISMISS = 'OneSignal-inAppMessageDidDismiss'; export const PERMISSION_CHANGED = 'OneSignal-permissionChanged'; export const SUBSCRIPTION_CHANGED = 'OneSignal-subscriptionChanged'; +export const USER_STATE_CHANGED = 'OneSignal-userStateChanged'; diff --git a/src/index.ts b/src/index.ts index 1958466f..3979bd05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { NOTIFICATION_WILL_DISPLAY, PERMISSION_CHANGED, SUBSCRIPTION_CHANGED, + USER_STATE_CHANGED, } from './events/events'; import { NotificationEventName, @@ -23,6 +24,7 @@ import { OSNotificationPermission, PushSubscriptionChangedState, } from './models/Subscription'; +import { UserState, UserChangedState } from './models/User'; import NotificationWillDisplayEvent from './events/NotificationWillDisplayEvent'; import { InAppMessage, @@ -310,6 +312,51 @@ export namespace OneSignal { RNOneSignal.optIn(); } } + + /** + * Add a callback 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. + */ + export function addEventListener( + event: 'change', + listener: (event: UserChangedState) => void, + ) { + if (!isNativeModuleLoaded(RNOneSignal)) return; + + isValidCallback(listener); + RNOneSignal.addUserStateObserver(); + eventManager.addEventListener( + USER_STATE_CHANGED, + listener, + ); + } + + /** Clears current user state observers. */ + export function removeEventListener( + event: 'change', + listener: (event: UserChangedState) => void, + ) { + if (!isNativeModuleLoaded(RNOneSignal)) return; + + eventManager.removeEventListener(USER_STATE_CHANGED, listener); + } + + /** Get the nullable OneSignal Id associated with the user. */ + export async function getOnesignalId(): Promise { + if (!isNativeModuleLoaded(RNOneSignal)) { + return Promise.reject(new Error('OneSignal native module not loaded')); + } + return RNOneSignal.getOnesignalId(); + } + + /** Get the nullable External Id associated with the user. */ + export async function getExternalId(): Promise { + if (!isNativeModuleLoaded(RNOneSignal)) { + return Promise.reject(new Error('OneSignal native module not loaded')); + } + return RNOneSignal.getExternalId(); + } + /** Explicitly set a 2-character language code for the user. */ export function setLanguage(language: string) { if (!isNativeModuleLoaded(RNOneSignal)) return; @@ -852,6 +899,8 @@ export { InAppMessageDidDismissEvent, PushSubscriptionState, PushSubscriptionChangedState, + UserState, + UserChangedState, OSNotificationPermission, }; diff --git a/src/models/User.ts b/src/models/User.ts new file mode 100644 index 00000000..465121f2 --- /dev/null +++ b/src/models/User.ts @@ -0,0 +1,8 @@ +export interface UserState { + externalId?: string; + onesignalId?: string; +} + +export interface UserChangedState { + current: UserState; +}