diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java index 8d82807c91..7c48f4b070 100644 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java +++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java @@ -8,7 +8,6 @@ import androidx.multidex.MultiDexApplication; import com.onesignal.OneSignal; -import com.onesignal.inAppMessages.IInAppMessage; import com.onesignal.inAppMessages.IInAppMessageClickListener; import com.onesignal.inAppMessages.IInAppMessageClickEvent; import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent; @@ -26,6 +25,9 @@ import com.onesignal.sdktest.constant.Text; import com.onesignal.sdktest.notification.OneSignalNotificationSender; import com.onesignal.sdktest.util.SharedPreferenceUtil; +import com.onesignal.user.state.IUserStateObserver; +import com.onesignal.user.state.UserChangedState; +import com.onesignal.user.state.UserState; import org.json.JSONObject; @@ -116,6 +118,14 @@ public void onWillDisplay(@NonNull INotificationWillDisplayEvent event) { } }); + OneSignal.getUser().addObserver(new IUserStateObserver() { + @Override + public void onUserStateChange(@NonNull UserChangedState state) { + UserState currentUserState = state.getCurrent(); + Log.v(Tag.LOG_TAG, "onUserStateChange fired " + currentUserState.toJSONObject()); + } + }); + OneSignal.getInAppMessages().setPaused(true); OneSignal.getLocation().setShared(false); diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java index 30508b0d35..58069a298c 100644 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java +++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java @@ -3,7 +3,6 @@ import android.app.Activity; import android.content.Context; import com.google.android.material.appbar.AppBarLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -13,10 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.appcompat.widget.Toolbar; - import android.content.Intent; import android.os.Build; -import android.util.Log; import android.util.Pair; import android.view.View; import android.view.ViewTreeObserver; @@ -25,13 +22,10 @@ import android.widget.RelativeLayout; import android.widget.Switch; import android.widget.TextView; - import com.onesignal.Continue; import com.onesignal.OneSignal; import com.onesignal.sdktest.adapter.SubscriptionRecyclerViewAdapter; -import com.onesignal.user.subscriptions.IEmailSubscription; import com.onesignal.user.subscriptions.IPushSubscription; -import com.onesignal.user.subscriptions.ISmsSubscription; import com.onesignal.sdktest.R; import com.onesignal.sdktest.activity.SecondaryActivity; import com.onesignal.sdktest.adapter.InAppMessageRecyclerViewAdapter; @@ -57,7 +51,6 @@ import com.onesignal.user.subscriptions.ISubscription; import com.onesignal.user.subscriptions.IPushSubscriptionObserver; import com.onesignal.user.subscriptions.PushSubscriptionChangedState; - import java.util.ArrayList; import java.util.HashMap; import java.util.Map; diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6414f29a10..94a2ec5c31 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -259,8 +259,12 @@ The user name space is accessible via `OneSignal.User` (in Kotlin) or `OneSignal | `val pushSubscription: IPushSubscription` | `IPushSubscription getPushSubscription()` | *The push subscription associated to the current user.* | | `fun setLanguage(value: String)` | `void setLanguage(String value)` | *Set the 2-character language either as a detected language or explicitly set for this user.* | | `fun pushSubscription.addChangeHandler(handler: ISubscriptionChangedHandler)` | `void pushSubscription.addChangeHandler(ISubscriptionChangedHandler handler)` | *Adds a change handler that will run whenever the push subscription has been changed.* | +| `val onesignalId: String` | `String getOnesignalId()` | *Returns the OneSignal ID for the current user, which can be the empty string if it is not yet available.* | +| `val externalId: String` | `String getExternalId()` | *Returns the external ID for the current user, which can be the empty string if not set.* | +| `fun addObserver(observer: IUserStateObserver)` | `void addObserve(IUserStateObserver observer)` | *The `IUserStateObserver.onUserStateChange` method will be fired on the passed-in object when the user state changes. The User State contains the onesignalId and externalId (which can be empty strings), and the observer will be fired when these values change.* | +| `fun removeObserver(observer: IUserStateObserver)` | `void removeObserver(IUserStateObserver observer)` | *Remove a user state observer that has been previously added.* | | `fun addAlias(label: String, id: String)` | `void addAlias(String label, String id)` | *Set an alias for the current user. If this alias already exists it will be overwritten.* | -| `fun addAliases(aliases: Map)` | `void addAliases(Map aliases)` | S*et aliases for the current user. If any alias already exists it will be overwritten.* | +| `fun addAliases(aliases: Map)` | `void addAliases(Map aliases)` | *Set aliases for the current user. If any alias already exists it will be overwritten.* | | `fun removeAlias(label: String)` | `void removeAlias(String label)` | *Remove an alias from the current user.* | | `fun removeAliases(labels: Collection)` | `void removeAliases(Collection labels)` | *Remove multiple aliases from the current user.* | | `fun addEmail(email: String)` | `void addEmail(String email)` | *Add a new email subscription to the current user.* | diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt index b3e2bc3864..2b71ca11de 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt @@ -1,6 +1,7 @@ package com.onesignal.user import com.onesignal.OneSignal +import com.onesignal.user.state.IUserStateObserver import com.onesignal.user.subscriptions.IPushSubscription /** @@ -25,6 +26,19 @@ interface IUserManager { */ val pushSubscription: IPushSubscription + /** + * The UUID generated by OneSignal to represent a user, empty if this is currently unavailable + */ + val onesignalId: String + + /** + * The External ID is OneSignal's default and recommended alias label. This should be the main + * identifier you use to identify users. It is set when calling the [OneSignal.login] method. + * + * This is empty if the External ID has not been set. + */ + val externalId: String + /** * Set the 2-character language either as a detected language or explicitly set for this user. See * See [Supported Languages | OneSignal](https://documentation.onesignal.com/docs/language-localization#what-languages-are-supported) @@ -138,4 +152,18 @@ interface IUserManager { * Return a copy of all local tags from the current user. */ fun getTags(): Map + + /** + * Add an observer to the user state, allowing the provider to be + * notified whenever the user state has changed. + * + * Important: When using the observer to retrieve the onesignalId, check the externalId as well + * to confirm the values are associated with the expected user. + */ + fun addObserver(observer: IUserStateObserver) + + /** + * Remove an observer from the user state. + */ + fun removeObserver(observer: IUserStateObserver) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index 0effb3c9ed..934d233183 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -1,6 +1,10 @@ package com.onesignal.user.internal +import com.onesignal.common.IDManager import com.onesignal.common.OneSignalUtils +import com.onesignal.common.events.EventProducer +import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler +import com.onesignal.common.modeling.ModelChangedArgs import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging @@ -12,6 +16,9 @@ import com.onesignal.user.internal.properties.PropertiesModel import com.onesignal.user.internal.properties.PropertiesModelStore import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionList +import com.onesignal.user.state.IUserStateObserver +import com.onesignal.user.state.UserChangedState +import com.onesignal.user.state.UserState import com.onesignal.user.subscriptions.IPushSubscription internal open class UserManager( @@ -19,9 +26,12 @@ internal open class UserManager( private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _languageContext: ILanguageContext, -) : IUserManager { - val externalId: String? - get() = _identityModel.externalId +) : IUserManager, ISingletonModelStoreChangeHandler { + override val onesignalId: String + get() = if (IDManager.isLocalId(_identityModel.onesignalId)) "" else _identityModel.onesignalId + + override val externalId: String + get() = _identityModel.externalId ?: "" val aliases: Map get() = _identityModel.filter { it.key != IdentityModel::id.name }.toMap() @@ -29,6 +39,8 @@ internal open class UserManager( val subscriptions: SubscriptionList get() = _subscriptionManager.subscriptions + val changeHandlersNotifier = EventProducer() + override val pushSubscription: IPushSubscription get() = _subscriptionManager.subscriptions.push @@ -42,6 +54,10 @@ internal open class UserManager( _languageContext.language = value } + init { + _identityModelStore.subscribe(this) + } + override fun addAlias( label: String, id: String, @@ -219,4 +235,29 @@ internal open class UserManager( override fun getTags(): Map { return _propertiesModel.tags.toMap() } + + override fun addObserver(observer: IUserStateObserver) { + changeHandlersNotifier.subscribe(observer) + } + + override fun removeObserver(observer: IUserStateObserver) { + changeHandlersNotifier.unsubscribe(observer) + } + + override fun onModelReplaced( + model: IdentityModel, + tag: String, + ) { } + + override fun onModelUpdated( + args: ModelChangedArgs, + tag: String, + ) { + if (args.property == IdentityConstants.ONESIGNAL_ID) { + val newUserState = UserState(args.newValue.toString(), externalId) + this.changeHandlersNotifier.fire { + it.onUserStateChange(UserChangedState(newUserState)) + } + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/IUserStateObserver.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/IUserStateObserver.kt new file mode 100644 index 0000000000..aa25d9106e --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/IUserStateObserver.kt @@ -0,0 +1,15 @@ +package com.onesignal.user.state + +/** + * A user state changed observer. Implement this interface and provide the implementation + * to be notified when the user state has changed. + */ +interface IUserStateObserver { + /** + * Called when the user state this change handler was added to, has changed. A + * user state can change when user has logged in or out + * + * @param state The user changed state. + */ + fun onUserStateChange(state: UserChangedState) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserChangedState.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserChangedState.kt new file mode 100644 index 0000000000..aef57d55bb --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserChangedState.kt @@ -0,0 +1,12 @@ +package com.onesignal.user.state + +import org.json.JSONObject + +class UserChangedState( + val current: UserState, +) { + fun toJSONObject(): JSONObject { + return JSONObject() + .put("current", current.toJSONObject()) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserState.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserState.kt new file mode 100644 index 0000000000..ea986e84c6 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/state/UserState.kt @@ -0,0 +1,27 @@ +package com.onesignal.user.state + +import org.json.JSONObject + +/** + * A user state. + */ +class UserState( + /** + * The unique identifier for your OneSignal account. This will be an empty string until the + * user has been successfully logged in on the backend and assigned an ID. + * Use [addObserver] to be notified when the [onesignalId] has been successfully assigned. + */ + val onesignalId: String, + /** + * The external identifier that you use to identify users. Use [addObserver] to be notified + * when the [externalId] has been successfully assigned. This will be an empty string if no + * external identifier has been assigned to the associated [onesignalId]. + */ + val externalId: String, +) { + fun toJSONObject(): JSONObject { + return JSONObject() + .put("onesignalId", onesignalId) + .put("externalId", externalId) + } +} diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/mocks/MockHelper.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/mocks/MockHelper.kt index 1974ff9a3c..58cf3a3f82 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/mocks/MockHelper.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/mocks/MockHelper.kt @@ -65,7 +65,7 @@ object MockHelper { action(identityModel) } - val mockIdentityStore = mockk() + val mockIdentityStore = mockk(relaxed = true) every { mockIdentityStore.model } returns identityModel diff --git a/OneSignalSDK/onesignal/notifications/consumer-rules.pro b/OneSignalSDK/onesignal/notifications/consumer-rules.pro index d9fc948356..2d829efe1b 100644 --- a/OneSignalSDK/onesignal/notifications/consumer-rules.pro +++ b/OneSignalSDK/onesignal/notifications/consumer-rules.pro @@ -20,6 +20,10 @@ void onPushSubscriptionChange(com.onesignal.user.subscriptions.PushSubscriptionChangedState); } +-keep class ** implements com.onesignal.user.state.IUserStateObserver { + void onUserStateChange(com.onesignal.user.state.UserChangedState); +} + -keep class ** implements com.onesignal.notifications.INotificationServiceExtension{ void onNotificationReceived(com.onesignal.notifications.INotificationReceivedEvent); }