From 024bf16d143e7ea7b98f0ac478ec9b0d4900b910 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:36:48 -0400 Subject: [PATCH 01/12] Infrastructure change around JWT: models, model stores --- .../onesignal/sdktest/util/ProfileUtil.java | 7 +++++++ .../core/internal/config/ConfigModel.kt | 20 +++++++++++++++++++ .../config/impl/ConfigModelStoreListener.kt | 3 ++- .../user/internal/identity/IdentityModel.kt | 11 ++++++++++ .../internal/identity/IdentityModelStore.kt | 6 +++++- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java index 9a026c5f47..c48edd4d4f 100644 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java +++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java @@ -13,6 +13,7 @@ public enum FieldType { ALIAS("Alias"), EMAIL("Email"), SMS("SMS"), + JWT("JWT"), EXTERNAL_USER_ID("External User Id"), TAG("Tags"), @@ -97,6 +98,10 @@ public static boolean isSMSValid(TextInputLayout smsTextInputLayout) { return true; } + private static boolean isJWTValid(TextInputLayout jwtTextInputLayout) { + return !jwtTextInputLayout.getEditText().toString().isEmpty(); + } + private static boolean isExternalUserIdValid(TextInputLayout externalUserIdTextInputLayout) { externalUserIdTextInputLayout.setErrorEnabled(false); if (externalUserIdTextInputLayout.getEditText() != null) { @@ -137,6 +142,8 @@ static boolean isContentValid(FieldType field, TextInputLayout alertDialogTextIn return isEmailValid(alertDialogTextInputLayout); case SMS: return isSMSValid(alertDialogTextInputLayout); + case JWT: + return isJWTValid(alertDialogTextInputLayout); case EXTERNAL_USER_ID: return isExternalUserIdValid(alertDialogTextInputLayout); case TAG: diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt index 74d31c4669..0e25c480c3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt @@ -1,6 +1,8 @@ package com.onesignal.core.internal.config +import com.onesignal.common.events.EventProducer import com.onesignal.common.modeling.Model +import com.onesignal.core.internal.backend.ParamsObject import org.json.JSONArray import org.json.JSONObject @@ -319,6 +321,20 @@ class ConfigModel : Model() { return null } + + var fetchParamsNotifier = EventProducer() + + fun addFetchParamsObserver(observer: FetchParamsObserver) { + fetchParamsNotifier.subscribe(observer) + } + + fun removeFetchParamsObserver(observer: FetchParamsObserver) { + fetchParamsNotifier.unsubscribe(observer) + } + + fun notifyFetchParams(params: ParamsObject) { + fetchParamsNotifier.fire { it.onParamsFetched(params) } + } } /** @@ -425,3 +441,7 @@ class FCMConfigModel(parentModel: Model, parentProperty: String) : Model(parentM setOptStringProperty(::apiKey.name, value) } } + +interface FetchParamsObserver { + fun onParamsFetched(params: ParamsObject) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt index 87d7eae6b0..8622e05c90 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt @@ -72,7 +72,7 @@ internal class ConfigModelStoreListener( // copy current model into new model, then override with what comes down. val config = ConfigModel() config.initializeFromModel(null, _configModelStore.model) - + config.fetchParamsNotifier = _configModelStore.model.fetchParamsNotifier config.isInitializedWithRemote = true // these are always copied from the backend params @@ -105,6 +105,7 @@ internal class ConfigModelStoreListener( _configModelStore.replace(config, ModelChangeTags.HYDRATE) success = true + config.notifyFetchParams(params) } catch (ex: BackendException) { if (ex.statusCode == HttpURLConnection.HTTP_FORBIDDEN) { Logging.fatal("403 error getting OneSignal params, omitting further retries!") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModel.kt index d443fdad60..70a5015f2e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModel.kt @@ -1,6 +1,7 @@ package com.onesignal.user.internal.identity import com.onesignal.common.modeling.MapModel +import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.user.internal.backend.IdentityConstants /** @@ -29,4 +30,14 @@ class IdentityModel : MapModel() { set(value) { setOptStringProperty(IdentityConstants.EXTERNAL_ID, value) } + + /** + * A JWT token generated on your server and given to a OneSignal Client SDK so it can manage + * a specific User, their Subscriptions, and Identities (AKA add/remove Aliases). + */ + var jwtToken: String? + get() = getOptStringProperty(IdentityConstants.JWT_TOKEN) + set(value) { + setOptStringProperty(IdentityConstants.JWT_TOKEN, value, ModelChangeTags.NO_PROPOGATE) + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt index 911c4ba71b..6e761bbd4d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt @@ -6,4 +6,8 @@ import com.onesignal.core.internal.preferences.IPreferencesService open class IdentityModelStore(prefs: IPreferencesService) : SingletonModelStore( SimpleModelStore({ IdentityModel() }, "identity", prefs), -) +) { + fun invalidateJwt() { + model.jwtToken = "" + } +} From 5713fe793e7fe5965c38d3604854d1d2b3a4845c Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:40:46 -0400 Subject: [PATCH 02/12] Public API impl for JWT, User Manager, and callbacks --- .../src/main/java/com/onesignal/IOneSignal.kt | 17 +++++ .../onesignal/IUserJwtInvalidatedListener.kt | 16 +++++ .../src/main/java/com/onesignal/OneSignal.kt | 25 +++++++ .../com/onesignal/UserJwtInvalidatedEvent.kt | 10 +++ .../com/onesignal/internal/OneSignalImp.kt | 65 ++++++++++++++++++- .../java/com/onesignal/user/IUserManager.kt | 5 ++ .../onesignal/user/internal/UserManager.kt | 35 ++++++++-- 7 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt index cb707e4fe4..ce60849a6d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt @@ -19,6 +19,11 @@ interface IOneSignal { */ val isInitialized: Boolean + /** + * Whether the security feature to authenticate your external user ids is enabled + */ + val useIdentityVerification: Boolean + /** * The user manager for accessing user-scoped * management. @@ -123,4 +128,16 @@ interface IOneSignal { * data is not cleared. */ fun logout() + + /** + * Update JWT token for a user + */ + fun updateUserJwt( + externalId: String, + token: String, + ) + + fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) + + fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt new file mode 100644 index 0000000000..7abdf10849 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt @@ -0,0 +1,16 @@ +package com.onesignal + +/** TODO: complete the comment part for this listener + * Implement this interface and provide an instance to [OneSignal.addUserJwtInvalidatedListner] + * in order to receive control when the JWT for the current user is invalidated. + * + * @see [User JWT Invalidated Event | OneSignal Docs](https://documentation.onesignal.com/docs/) + */ +interface IUserJwtInvalidatedListener { + /** + * Called when the JWT is invalidated + * + * @param event The user JWT that expired. + */ + fun onUserJwtInvalidated(event: UserJwtInvalidatedEvent) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt index 580cd63252..799c50c9c9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt @@ -29,6 +29,13 @@ object OneSignal { val isInitialized: Boolean get() = oneSignal.isInitialized + /** + * Whether the security feature to authenticate your external user ids is enabled + */ + @JvmStatic + val useIdentityVerification: Boolean + get() = oneSignal.useIdentityVerification + /** * The current SDK version as a string. */ @@ -192,6 +199,24 @@ object OneSignal { @JvmStatic fun logout() = oneSignal.logout() + @JvmStatic + fun updateUserJwt( + externalId: String, + token: String, + ) { + oneSignal.updateUserJwt(externalId, token) + } + + @JvmStatic + fun addUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) { + oneSignal.addUserJwtInvalidatedListener(listener) + } + + @JvmStatic + fun removeUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) { + oneSignal.removeUserJwtInvalidatedListener(listener) + } + private val oneSignal: IOneSignal by lazy { OneSignalImp() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt new file mode 100644 index 0000000000..986d2eb5c9 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt @@ -0,0 +1,10 @@ +package com.onesignal + +/** TODO: jwt documentation + * The event passed into [IUserJwtInvalidatedListener.onUserJwtInvalidated], it provides access + * to the external ID whose JWT has just been invalidated. + * + */ +class UserJwtInvalidatedEvent( + val externalId: String, +) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index bb0e5a11d2..4d91e968a4 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -3,6 +3,7 @@ package com.onesignal.internal import android.content.Context import android.os.Build import com.onesignal.IOneSignal +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.common.AndroidUtils import com.onesignal.common.DeviceUtils import com.onesignal.common.IDManager @@ -18,8 +19,10 @@ import com.onesignal.common.threading.suspendifyOnThread import com.onesignal.core.CoreModule import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.application.impl.ApplicationService +import com.onesignal.core.internal.backend.ParamsObject import com.onesignal.core.internal.config.ConfigModel import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.config.FetchParamsObserver import com.onesignal.core.internal.operations.IOperationRepo import com.onesignal.core.internal.preferences.IPreferencesService import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys @@ -44,6 +47,7 @@ import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation import com.onesignal.user.internal.operations.LoginUserOperation +import com.onesignal.user.internal.operations.RefreshUserOperation import com.onesignal.user.internal.operations.TransferSubscriptionOperation import com.onesignal.user.internal.properties.PropertiesModel import com.onesignal.user.internal.properties.PropertiesModelStore @@ -56,6 +60,8 @@ import org.json.JSONObject internal class OneSignalImp : IOneSignal, IServiceProvider { override val sdkVersion: String = OneSignalUtils.SDK_VERSION override var isInitialized: Boolean = false + override val useIdentityVerification: Boolean + get() = configModel?.useIdentityVerification ?: true override var consentRequired: Boolean get() = configModel?.consentRequired ?: (_consentRequired == true) @@ -245,6 +251,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { // bootstrap services startupService.bootstrap() + resumeOperationRepoAfterFetchParams(configModel!!) if (forceCreateUser || !identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) { val legacyPlayerId = preferencesService!!.getString( @@ -282,7 +289,8 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { pushSubscriptionModel.id = legacyPlayerId pushSubscriptionModel.type = SubscriptionType.PUSH pushSubscriptionModel.optedIn = - notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value + notificationTypes != SubscriptionStatus.NO_PERMISSION.value && + notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value pushSubscriptionModel.address = legacyUserSyncJSON.safeString("identifier") ?: "" if (notificationTypes != null) { @@ -355,12 +363,23 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { currentIdentityOneSignalId = identityModelStore!!.model.onesignalId if (currentIdentityExternalId == externalId) { + // login is for same user that is already logged in, fetch (refresh) + // the current user. + identityModelStore!!.model.jwtToken = jwtBearerToken + operationRepo!!.enqueue( + RefreshUserOperation( + configModel!!.appId, + identityModelStore!!.model.onesignalId, + ), + true, + ) return } // TODO: Set JWT Token for all future requests. createAndSwitchToNewUser { identityModel, _ -> identityModel.externalId = externalId + identityModel.jwtToken = jwtBearerToken } newIdentityOneSignalId = identityModelStore!!.model.onesignalId @@ -403,6 +422,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { return } + // calling createAndSwitchToNewUser() replaces model with a default empty jwt createAndSwitchToNewUser() operationRepo!!.enqueue( LoginUserOperation( @@ -411,9 +431,33 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { identityModelStore!!.model.externalId, ), ) + } + } - // TODO: remove JWT Token for all future requests. + override fun updateUserJwt( + externalId: String, + token: String, + ) { + // update the model with the given externalId + for (model in identityModelStore!!.store.list()) { + if (externalId == model.externalId) { + identityModelStore!!.model.jwtToken = token + operationRepo!!.setPaused(false) + operationRepo!!.forceExecuteOperations() + Logging.log(LogLevel.DEBUG, "JWT $token is updated for externalId $externalId") + return + } } + + Logging.log(LogLevel.DEBUG, "No identity found for externalId $externalId") + } + + override fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + user.addUserJwtInvalidatedListner(listener) + } + + override fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + user.removeUserJwtInvalidatedListner(listener) } private fun createAndSwitchToNewUser( @@ -481,6 +525,23 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { } } + private fun resumeOperationRepoAfterFetchParams(configModel: ConfigModel) { + // pause operation repo until useIdentityVerification is determined + operationRepo!!.setPaused(true) + configModel.addFetchParamsObserver( + object : FetchParamsObserver { + override fun onParamsFetched(params: ParamsObject) { + // resume operations if identity verification is turned off or a jwt is cached + if (params.useIdentityVerification == false || identityModelStore!!.model.jwtToken != null) { + operationRepo!!.setPaused(false) + } else { + Logging.log(LogLevel.ERROR, "A valid JWT is required for user ${identityModelStore!!.model.externalId}.") + } + } + }, + ) + } + override fun hasService(c: Class): Boolean = services.hasService(c) override fun getService(c: Class): T = services.getService(c) 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 2b71ca11de..43417a05b6 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,5 +1,6 @@ package com.onesignal.user +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.OneSignal import com.onesignal.user.state.IUserStateObserver import com.onesignal.user.subscriptions.IPushSubscription @@ -166,4 +167,8 @@ interface IUserManager { * Remove an observer from the user state. */ fun removeObserver(observer: IUserStateObserver) + + fun addUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) + + fun removeUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) } 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 934d233183..bba9120513 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,5 +1,8 @@ package com.onesignal.user.internal +import com.onesignal.IUserJwtInvalidatedListener +import com.onesignal.OneSignal +import com.onesignal.UserJwtInvalidatedEvent import com.onesignal.common.IDManager import com.onesignal.common.OneSignalUtils import com.onesignal.common.events.EventProducer @@ -41,6 +44,8 @@ internal open class UserManager( val changeHandlersNotifier = EventProducer() + val jwtInvalidatedCallback = EventProducer() + override val pushSubscription: IPushSubscription get() = _subscriptionManager.subscriptions.push @@ -244,6 +249,16 @@ internal open class UserManager( changeHandlersNotifier.unsubscribe(observer) } + override fun addUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) { + Logging.debug("OneSignal.addClickListener(listener: $listener)") + jwtInvalidatedCallback.subscribe(listener) + } + + override fun removeUserJwtInvalidatedListner(listener: IUserJwtInvalidatedListener) { + Logging.debug("OneSignal.removeClickListener(listener: $listener)") + jwtInvalidatedCallback.unsubscribe(listener) + } + override fun onModelReplaced( model: IdentityModel, tag: String, @@ -253,10 +268,22 @@ internal open class UserManager( args: ModelChangedArgs, tag: String, ) { - if (args.property == IdentityConstants.ONESIGNAL_ID) { - val newUserState = UserState(args.newValue.toString(), externalId) - this.changeHandlersNotifier.fire { - it.onUserStateChange(UserChangedState(newUserState)) + when (args.property) { + IdentityConstants.ONESIGNAL_ID -> { + val newUserState = UserState(args.newValue.toString(), externalId) + this.changeHandlersNotifier.fire { + it.onUserStateChange(UserChangedState(newUserState)) + } + } + IdentityConstants.JWT_TOKEN -> { + // Fire the event when the JWT has been invalidated. + val oldJwt = args.oldValue.toString() + val newJwt = args.newValue.toString() + if (OneSignal.useIdentityVerification && oldJwt != newJwt && newJwt.isEmpty()) { + jwtInvalidatedCallback.fire { + it.onUserJwtInvalidated(UserJwtInvalidatedEvent((externalId))) + } + } } } } From 1066393b43b7455b6caeb0ae8896e1f971192c74 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:43:46 -0400 Subject: [PATCH 03/12] Add JWT to HttpClient, backend services, and operations --- .../backend/impl/ParamsBackendService.kt | 2 +- .../core/internal/http/IHttpClient.kt | 9 ++++- .../core/internal/http/impl/HttpClient.kt | 35 ++++++++++++++----- .../backend/IIdentityBackendService.kt | 7 ++++ .../backend/ISubscriptionBackendService.kt | 5 +++ .../internal/backend/IUserBackendService.kt | 3 ++ .../backend/impl/IdentityBackendService.kt | 6 ++-- .../impl/SubscriptionBackendService.kt | 15 +++++--- .../backend/impl/UserBackendService.kt | 9 +++-- .../operations/UpdateSubscriptionOperation.kt | 14 +++++++- 10 files changed, 83 insertions(+), 22 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt index 90da4481b2..a285833c22 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt @@ -31,7 +31,7 @@ internal class ParamsBackendService( paramsUrl += "?player_id=$subscriptionId" } - val response = _http.get(paramsUrl, CacheKeys.REMOTE_PARAMS) + val response = _http.get(paramsUrl, CacheKeys.REMOTE_PARAMS, "") if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/IHttpClient.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/IHttpClient.kt index 7232b43a34..2f131f03d3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/IHttpClient.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/IHttpClient.kt @@ -18,6 +18,7 @@ interface IHttpClient { suspend fun post( url: String, body: JSONObject, + jwt: String? = null, ): HttpResponse /** @@ -34,6 +35,7 @@ interface IHttpClient { suspend fun get( url: String, cacheKey: String? = null, + jwt: String? = null, ): HttpResponse /** @@ -47,6 +49,7 @@ interface IHttpClient { suspend fun put( url: String, body: JSONObject, + jwt: String? = null, ): HttpResponse /** @@ -60,6 +63,7 @@ interface IHttpClient { suspend fun patch( url: String, body: JSONObject, + jwt: String? = null, ): HttpResponse /** @@ -69,7 +73,10 @@ interface IHttpClient { * * @return The response returned. */ - suspend fun delete(url: String): HttpResponse + suspend fun delete( + url: String, + jwt: String? = null, + ): HttpResponse } /** diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt index a3827cb19b..3bbbfe56e9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt @@ -45,33 +45,40 @@ internal class HttpClient( override suspend fun post( url: String, body: JSONObject, + jwt: String?, ): HttpResponse { - return makeRequest(url, "POST", body, _configModelStore.model.httpTimeout, null) + return makeRequest(url, "POST", body, _configModelStore.model.httpTimeout, null, jwt) } override suspend fun get( url: String, cacheKey: String?, + jwt: String?, ): HttpResponse { - return makeRequest(url, null, null, _configModelStore.model.httpGetTimeout, cacheKey) + return makeRequest(url, null, null, _configModelStore.model.httpGetTimeout, cacheKey, jwt) } override suspend fun put( url: String, body: JSONObject, + jwt: String?, ): HttpResponse { - return makeRequest(url, "PUT", body, _configModelStore.model.httpTimeout, null) + return makeRequest(url, "PUT", body, _configModelStore.model.httpTimeout, null, jwt) } override suspend fun patch( url: String, body: JSONObject, + jwt: String?, ): HttpResponse { - return makeRequest(url, "PATCH", body, _configModelStore.model.httpTimeout, null) + return makeRequest(url, "PATCH", body, _configModelStore.model.httpTimeout, null, jwt) } - override suspend fun delete(url: String): HttpResponse { - return makeRequest(url, "DELETE", null, _configModelStore.model.httpTimeout, null) + override suspend fun delete( + url: String, + jwt: String?, + ): HttpResponse { + return makeRequest(url, "DELETE", null, _configModelStore.model.httpTimeout, null, jwt) } private suspend fun makeRequest( @@ -80,6 +87,7 @@ internal class HttpClient( jsonBody: JSONObject?, timeout: Int, cacheKey: String?, + jwt: String? = null, ): HttpResponse { // If privacy consent is required but not yet given, any non-GET request should be blocked. if (method != null && _configModelStore.model.consentRequired == true && _configModelStore.model.consentGiven != true) { @@ -94,7 +102,7 @@ internal class HttpClient( try { return withTimeout(getThreadTimeout(timeout).toLong()) { - return@withTimeout makeRequestIODispatcher(url, method, jsonBody, timeout, cacheKey) + return@withTimeout makeRequestIODispatcher(url, method, jsonBody, timeout, cacheKey, jwt) } } catch (e: TimeoutCancellationException) { Logging.error("HttpClient: Request timed out: $url", e) @@ -111,6 +119,7 @@ internal class HttpClient( jsonBody: JSONObject?, timeout: Int, cacheKey: String?, + jwt: String? = null, ): HttpResponse { var retVal: HttpResponse? = null @@ -141,6 +150,10 @@ internal class HttpClient( con.readTimeout = timeout con.setRequestProperty("SDK-Version", "onesignal/android/" + OneSignalUtils.SDK_VERSION) + if (!jwt.isNullOrEmpty()) { + con.setRequestProperty("Authorization", "Bearer $jwt") + } + if (OneSignalWrapper.sdkType != null && OneSignalWrapper.sdkVersion != null) { con.setRequestProperty("SDK-Wrapper", "onesignal/${OneSignalWrapper.sdkType}/${OneSignalWrapper.sdkVersion}") } @@ -197,7 +210,9 @@ internal class HttpClient( PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_HTTP_CACHE_PREFIX + cacheKey, ) - Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - Using Cached response due to 304: " + cachedResponse) + Logging.debug( + "HttpClient: Got Response = ${method ?: "GET"} ${con.url} - Using Cached response due to 304: " + cachedResponse, + ) // TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT? retVal = HttpResponse(httpResponse, cachedResponse, retryAfterSeconds = retryAfter) @@ -207,7 +222,9 @@ internal class HttpClient( val scanner = Scanner(inputStream, "UTF-8") val json = if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else "" scanner.close() - Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - STATUS: $httpResponse - Body: " + json) + Logging.debug( + "HttpClient: Got Response = ${method ?: "GET"} ${con.url} - STATUS: $httpResponse - Body: " + json, + ) if (cacheKey != null) { val eTag = con.getHeaderField("etag") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt index 880599871d..28308225d8 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt @@ -18,6 +18,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, identities: Map, + jwt: String? = null, ): Map /** @@ -35,6 +36,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String? = null, ) } @@ -48,4 +50,9 @@ internal object IdentityConstants { * The alias label for the internal onesignal ID alias. */ const val ONESIGNAL_ID = "onesignal_id" + + /** + * The alias label for the jwt token. + */ + const val JWT_TOKEN = "jwt_token" } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt index 8741b2df65..9b7a539eff 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt @@ -21,6 +21,7 @@ interface ISubscriptionBackendService { aliasLabel: String, aliasValue: String, subscription: SubscriptionObject, + jwt: String? = null, ): String? /** @@ -34,6 +35,7 @@ interface ISubscriptionBackendService { appId: String, subscriptionId: String, subscription: SubscriptionObject, + jwt: String? = null, ) /** @@ -45,6 +47,7 @@ interface ISubscriptionBackendService { suspend fun deleteSubscription( appId: String, subscriptionId: String, + jwt: String? = null, ) /** @@ -60,6 +63,7 @@ interface ISubscriptionBackendService { subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String? = null, ) /** @@ -73,5 +77,6 @@ interface ISubscriptionBackendService { suspend fun getIdentityFromSubscription( appId: String, subscriptionId: String, + jwt: String? = null, ): Map } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt index a47018641d..b08b59648b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt @@ -23,6 +23,7 @@ interface IUserBackendService { identities: Map, subscriptions: List, properties: Map, + jwt: String? = null, ): CreateUserResponse // TODO: Change to send only the push subscription, optimally @@ -47,6 +48,7 @@ interface IUserBackendService { properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, + jwt: String? = null, ) /** @@ -64,6 +66,7 @@ interface IUserBackendService { appId: String, aliasLabel: String, aliasValue: String, + jwt: String? = null, ): CreateUserResponse } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt index adfff7bdc9..9addf41eb2 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt @@ -15,12 +15,13 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, identities: Map, + jwt: String?, ): Map { val requestJSONObject = JSONObject() .put("identity", JSONObject().putMap(identities)) - val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject) + val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -36,8 +37,9 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete") + val response = _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete", jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt index bcb01db447..b5e8a20a1f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt @@ -16,12 +16,13 @@ internal class SubscriptionBackendService( aliasLabel: String, aliasValue: String, subscription: SubscriptionObject, + jwt: String?, ): String? { val jsonSubscription = JSONConverter.convertToJSON(subscription) jsonSubscription.remove("id") val requestJSON = JSONObject().put("subscription", jsonSubscription) - val response = _httpClient.post("apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions", requestJSON) + val response = _httpClient.post("apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions", requestJSON, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -40,12 +41,13 @@ internal class SubscriptionBackendService( appId: String, subscriptionId: String, subscription: SubscriptionObject, + jwt: String?, ) { val requestJSON = JSONObject() .put("subscription", JSONConverter.convertToJSON(subscription)) - val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId", requestJSON) + val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId", requestJSON, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -55,8 +57,9 @@ internal class SubscriptionBackendService( override suspend fun deleteSubscription( appId: String, subscriptionId: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId") + val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId", jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -68,12 +71,13 @@ internal class SubscriptionBackendService( subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String?, ) { val requestJSON = JSONObject() .put("identity", JSONObject().put(aliasLabel, aliasValue)) - val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON) + val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -83,8 +87,9 @@ internal class SubscriptionBackendService( override suspend fun getIdentityFromSubscription( appId: String, subscriptionId: String, + jwt: String?, ): Map { - val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity") + val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity", jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt index 45492aa501..6b26dd85f1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt @@ -18,6 +18,7 @@ internal class UserBackendService( identities: Map, subscriptions: List, properties: Map, + jwt: String?, ): CreateUserResponse { val requestJSON = JSONObject() @@ -36,7 +37,7 @@ internal class UserBackendService( requestJSON.put("refresh_device_metadata", true) - val response = _httpClient.post("apps/$appId/users", requestJSON) + val response = _httpClient.post("apps/$appId/users", requestJSON, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -52,6 +53,7 @@ internal class UserBackendService( properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, + jwt: String?, ) { val jsonObject = JSONObject() @@ -65,7 +67,7 @@ internal class UserBackendService( jsonObject.put("deltas", JSONConverter.convertToJSON(propertyiesDelta)) } - val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue", jsonObject) + val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue", jsonObject, jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -76,8 +78,9 @@ internal class UserBackendService( appId: String, aliasLabel: String, aliasValue: String, + jwt: String?, ): CreateUserResponse { - val response = _httpClient.get("apps/$appId/users/by/$aliasLabel/$aliasValue") + val response = _httpClient.get("apps/$appId/users/by/$aliasLabel/$aliasValue", jwt) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt index 59a9e1c1b7..f79706c1ff 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt @@ -81,13 +81,24 @@ class UpdateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.UP setEnumProperty(::status.name, value) } + /** + * The jwt token used for the operation that updates a subscription. + */ + var jwt: String? + get() = getStringProperty(::jwt.name) + private set(value) { + if (value != null) { + setStringProperty(::jwt.name, value!!) + } + } + override val createComparisonKey: String get() = "$appId.User.$onesignalId" override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Subscription.$subscriptionId" override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = subscriptionId - constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() { + constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus, jwt: String? = null) : this() { this.appId = appId this.onesignalId = onesignalId this.subscriptionId = subscriptionId @@ -95,6 +106,7 @@ class UpdateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.UP this.enabled = enabled this.address = address this.status = status + this.jwt = jwt } override fun translateIds(map: Map) { From f86dddf1f8e0c541991f07c78c65edb8413ceeae Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:45:33 -0400 Subject: [PATCH 04/12] Add UNAUTHORIZED handling to OperationRepo and operation executors --- .../internal/operations/IOperationRepo.kt | 2 ++ .../internal/operations/impl/OperationRepo.kt | 14 ++++++++++- .../executors/IdentityOperationExecutor.kt | 14 +++++++---- ...inUserFromSubscriptionOperationExecutor.kt | 5 +++- .../executors/LoginUserOperationExecutor.kt | 21 ++++++++++++++--- .../executors/RefreshUserOperationExecutor.kt | 11 ++++++--- .../SubscriptionOperationExecutor.kt | 23 +++++++++++++++---- .../executors/UpdateUserOperationExecutor.kt | 21 ++++++++++++++--- 8 files changed, 92 insertions(+), 19 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt index d2dceea5c3..3ab6059605 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt @@ -42,6 +42,8 @@ interface IOperationRepo { suspend fun awaitInitialized() fun forceExecuteOperations() + + fun setPaused(paused: Boolean) } // Extension function so the syntax containsInstanceOf() can be used over diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt index 7ed6336d4e..c033e56099 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt @@ -189,6 +189,10 @@ internal class OperationRepo( waiter.wake(LoopWaiterMessage(false)) } + override fun setPaused(paused: Boolean) { + this.paused = paused + } + /** * Waits until a new operation is enqueued, then wait an additional * amount of time afterwards, so operations can be grouped/batched. @@ -262,7 +266,15 @@ internal class OperationRepo( ops.forEach { _operationModelStore.remove(it.operation.id) } ops.forEach { it.waiter?.wake(true) } } - ExecutionResult.FAIL_UNAUTHORIZED, // TODO: Need to provide callback for app to reset JWT. For now, fail with no retry. + ExecutionResult.FAIL_UNAUTHORIZED -> { + Logging.error("Operation execution failed with invalid jwt, pausing the operation repo: $operations") + // keep the failed operation and pause the operation repo from executing + paused = true + // add back all operations to the front of the queue to be re-executed. + synchronized(queue) { + ops.reversed().forEach { queue.add(0, it) } + } + } ExecutionResult.FAIL_NORETRY, ExecutionResult.FAIL_CONFLICT, -> { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt index 104fe9569f..1b51f764d6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt @@ -50,6 +50,7 @@ internal class IdentityOperationExecutor( IdentityConstants.ONESIGNAL_ID, lastOperation.onesignalId, mapOf(lastOperation.label to lastOperation.value), + _identityModelStore.model.jwtToken, ) // ensure the now created alias is in the model as long as the user is still current. @@ -66,8 +67,10 @@ internal class IdentityOperationExecutor( ExecutionResponse(ExecutionResult.FAIL_NORETRY) NetworkUtils.ResponseStatusType.CONFLICT -> ExecutionResponse(ExecutionResult.FAIL_CONFLICT, retryAfterSeconds = ex.retryAfterSeconds) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> - ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() + return ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + } NetworkUtils.ResponseStatusType.MISSING -> { if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(lastOperation.onesignalId)) { return ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) @@ -93,6 +96,7 @@ internal class IdentityOperationExecutor( IdentityConstants.ONESIGNAL_ID, lastOperation.onesignalId, lastOperation.label, + _identityModelStore.model.jwtToken, ) // ensure the now deleted alias is not in the model as long as the user is still current. @@ -110,8 +114,10 @@ internal class IdentityOperationExecutor( ExecutionResponse(ExecutionResult.SUCCESS) NetworkUtils.ResponseStatusType.INVALID -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> - ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() + return ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + } NetworkUtils.ResponseStatusType.MISSING -> { return if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(lastOperation.onesignalId)) { ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt index 9a1178999d..50e097f87e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt @@ -46,6 +46,7 @@ internal class LoginUserFromSubscriptionOperationExecutor( _subscriptionBackend.getIdentityFromSubscription( loginUserOp.appId, loginUserOp.subscriptionId, + _identityModelStore.model.jwtToken, ) val backendOneSignalId = identities.getOrDefault(IdentityConstants.ONESIGNAL_ID, null) @@ -82,8 +83,10 @@ internal class LoginUserFromSubscriptionOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + } else -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt index e96c796513..a1d9b0747f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt @@ -126,6 +126,10 @@ internal class LoginUserOperationExecutor( ) createUser(loginUserOp, operations) } + ExecutionResult.FAIL_UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() + ExecutionResponse(result.result) + } else -> ExecutionResponse(result.result) } } @@ -160,7 +164,16 @@ internal class LoginUserOperationExecutor( try { val subscriptionList = subscriptions.toList() - val response = _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties) + val response = + _userBackend.createUser( + createUserOperation.appId, + identities, + subscriptionList.map { + it.second + }, + properties, + _identityModelStore.model.jwtToken, + ) val idTranslations = mutableMapOf() // Add the "local-to-backend" ID translation to the IdentifierTranslator for any operations that were // *not* executed but still reference the locally-generated IDs. @@ -212,8 +225,10 @@ internal class LoginUserOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> - ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + } else -> ExecutionResponse(ExecutionResult.FAIL_PAUSE_OPREPO) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt index cec48b05a9..1d23d2e39a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt @@ -59,6 +59,7 @@ internal class RefreshUserOperationExecutor( op.appId, IdentityConstants.ONESIGNAL_ID, op.onesignalId, + _identityModelStore.model.jwtToken, ) if (op.onesignalId != _identityModelStore.model.onesignalId) { @@ -98,7 +99,9 @@ internal class RefreshUserOperationExecutor( val subscriptionModel = SubscriptionModel() subscriptionModel.id = subscription.id!! subscriptionModel.address = subscription.token ?: "" - subscriptionModel.status = SubscriptionStatus.fromInt(subscription.notificationTypes ?: SubscriptionStatus.SUBSCRIBED.value) ?: SubscriptionStatus.SUBSCRIBED + subscriptionModel.status = SubscriptionStatus.fromInt( + subscription.notificationTypes ?: SubscriptionStatus.SUBSCRIBED.value, + ) ?: SubscriptionStatus.SUBSCRIBED subscriptionModel.type = when (subscription.type!!) { SubscriptionObjectType.EMAIL -> { @@ -147,8 +150,10 @@ internal class RefreshUserOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> - ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + } NetworkUtils.ResponseStatusType.MISSING -> { if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(op.onesignalId)) { return ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt index 73ee000745..7e549d9bed 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt @@ -22,6 +22,7 @@ import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService +import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.CreateSubscriptionOperation import com.onesignal.user.internal.operations.DeleteSubscriptionOperation import com.onesignal.user.internal.operations.TransferSubscriptionOperation @@ -35,6 +36,7 @@ internal class SubscriptionOperationExecutor( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, + private val _identityModelStore: IdentityModelStore, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, @@ -107,6 +109,7 @@ internal class SubscriptionOperationExecutor( IdentityConstants.ONESIGNAL_ID, createOperation.onesignalId, subscription, + _identityModelStore.model.jwtToken, ) ?: return ExecutionResponse(ExecutionResult.SUCCESS) // update the subscription model with the new ID, if it's still active. @@ -135,8 +138,10 @@ internal class SubscriptionOperationExecutor( NetworkUtils.ResponseStatusType.INVALID, -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + } NetworkUtils.ResponseStatusType.MISSING -> { if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(createOperation.onesignalId)) { return ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) @@ -145,7 +150,11 @@ internal class SubscriptionOperationExecutor( if (operations == null) { return ExecutionResponse(ExecutionResult.FAIL_NORETRY) } else { - return ExecutionResponse(ExecutionResult.FAIL_RETRY, operations = operations, retryAfterSeconds = ex.retryAfterSeconds) + return ExecutionResponse( + ExecutionResult.FAIL_RETRY, + operations = operations, + retryAfterSeconds = ex.retryAfterSeconds, + ) } } } @@ -175,7 +184,12 @@ internal class SubscriptionOperationExecutor( AndroidUtils.getAppVersion(_applicationService.appContext), ) - _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription) + _subscriptionBackend.updateSubscription( + lastOperation.appId, + lastOperation.subscriptionId, + subscription, + _identityModelStore.model.jwtToken, + ) } catch (ex: BackendException) { val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) @@ -223,6 +237,7 @@ internal class SubscriptionOperationExecutor( startingOperation.subscriptionId, IdentityConstants.ONESIGNAL_ID, startingOperation.onesignalId, + _identityModelStore.model.jwtToken, ) } catch (ex: BackendException) { val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) @@ -254,7 +269,7 @@ internal class SubscriptionOperationExecutor( private suspend fun deleteSubscription(op: DeleteSubscriptionOperation): ExecutionResponse { try { - _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId) + _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId, _identityModelStore.model.jwtToken) // remove the subscription model as a HYDRATE in case for some reason it still exists. _subscriptionModelStore.remove(op.subscriptionId, ModelChangeTags.HYDRATE) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt index e3d2a7425f..d77a5fa2db 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt @@ -83,7 +83,13 @@ internal class UpdateUserOperationExecutor( // that exist in this group. val sessionCount = if (deltasObject.sessionCount != null) deltasObject.sessionCount!! + 1 else 1 - deltasObject = PropertiesDeltasObject(deltasObject.sessionTime, sessionCount, deltasObject.amountSpent, deltasObject.purchases) + deltasObject = + PropertiesDeltasObject( + deltasObject.sessionTime, + sessionCount, + deltasObject.amountSpent, + deltasObject.purchases, + ) refreshDeviceMetadata = true } is TrackSessionEndOperation -> { @@ -96,7 +102,13 @@ internal class UpdateUserOperationExecutor( // operations that exist in this group. val sessionTime = if (deltasObject.sessionTime != null) deltasObject.sessionTime!! + operation.sessionTime else operation.sessionTime - deltasObject = PropertiesDeltasObject(sessionTime, deltasObject.sessionCount, deltasObject.amountSpent, deltasObject.purchases) + deltasObject = + PropertiesDeltasObject( + sessionTime, + deltasObject.sessionCount, + deltasObject.amountSpent, + deltasObject.purchases, + ) } is TrackPurchaseOperation -> { if (appId == null) { @@ -129,6 +141,7 @@ internal class UpdateUserOperationExecutor( propertiesObject, refreshDeviceMetadata, deltasObject, + _identityModelStore.model.jwtToken, ) if (_identityModelStore.model.onesignalId == onesignalId) { @@ -162,8 +175,10 @@ internal class UpdateUserOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) - NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> { + _identityModelStore.invalidateJwt() ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) + } NetworkUtils.ResponseStatusType.MISSING -> { if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(onesignalId)) { return ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) From 7df4a9546db81b61931f5a142da125a3240eea99 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:46:10 -0400 Subject: [PATCH 05/12] Add test units for JWT related change --- .../internal/operations/OperationRepoTests.kt | 49 +++++++++++++ .../onesignal/internal/OneSignalImpTests.kt | 12 ++++ .../user/internal/UserManagerTests.kt | 71 ++++++++++++++++++- .../SubscriptionOperationExecutorTests.kt | 30 +++++++- 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt index 936e0e5b92..85acc12b93 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt @@ -34,6 +34,12 @@ import java.util.UUID private class Mocks { val configModelStore = MockHelper.configModelStore() + val identityModelStore = + MockHelper.identityModelStore { + it.jwtToken = "" + it.externalId = "externalId1" + } + val operationModelStore: OperationModelStore = run { val operationStoreList = mutableListOf() @@ -63,6 +69,7 @@ private class Mocks { listOf(executor), operationModelStore, configModelStore, + identityModelStore, Time(), getNewRecordState(configModelStore), ), @@ -685,6 +692,48 @@ class OperationRepoTests : FunSpec({ response2 shouldBe true opRepo.forceExecuteOperations() } + + test("operations that need to be identity verified cannot execute until JWT is provided") { + // Given + val mocks = Mocks() + val waiter = Waiter() + + every { mocks.configModelStore.model.useIdentityVerification } returns true // set identity verification on + every { mocks.identityModelStore.model.jwtToken } returns null // jwt is initially unset + every { mocks.operationModelStore.remove(any()) } answers {} andThenAnswer { waiter.wake() } + + val operation1 = mockOperation("operationId1") + val operation2 = mockOperation("operationId2") + + operation1.setStringProperty("externalId", "externalId1") + operation2.setStringProperty("externalId", "externalId1") + + // When + mocks.operationRepo.enqueue(operation1) + mocks.operationRepo.enqueue(operation2) + mocks.operationRepo.start() + + waiter.waitForWake() + + // Then + coVerifyOrder { + mocks.operationModelStore.add(operation1) + mocks.operationModelStore.add(operation2) + } + + // + coVerify(exactly = 0) { + mocks.executor.execute( + withArg { + it.count() shouldBe 2 + it[0] shouldBe operation1 + it[1] shouldBe operation2 + }, + ) + mocks.operationModelStore.remove("operationId1") + mocks.operationModelStore.remove("operationId2") + } + } }) { companion object { private fun mockOperation( diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt index 420d67e6f1..ab0a748926 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt @@ -1,10 +1,12 @@ package com.onesignal.internal +import android.content.Context import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import io.mockk.mockk class OneSignalImpTests : FunSpec({ beforeAny { @@ -38,4 +40,14 @@ class OneSignalImpTests : FunSpec({ // Then exception.message shouldBe "Must call 'initWithContext' before 'logout'" } + + test("When identity verification is on and no user is created, calling initWithContext will create a new user") { + // Given + val os = OneSignalImp() + val appId = "tempAppId" + val context = mockk() + + // When + os.initWithContext(context, appId) + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt index 41e836eb9b..815a2311ea 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt @@ -1,17 +1,31 @@ package com.onesignal.user.internal +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.core.internal.language.ILanguageContext +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.core.internal.operations.Operation import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.backend.CreateUserResponse +import com.onesignal.user.internal.backend.IUserBackendService +import com.onesignal.user.internal.backend.IdentityConstants +import com.onesignal.user.internal.backend.PropertiesObject +import com.onesignal.user.internal.operations.LoginUserOperation +import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor +import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionList +import com.onesignal.user.internal.subscriptions.SubscriptionModelStore import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.slot +import io.mockk.spyk import io.mockk.verify class UserManagerTests : FunSpec({ @@ -141,7 +155,8 @@ class UserManagerTests : FunSpec({ it.tags["my-tag-key1"] = "my-tag-value1" } - val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) + val userManager = + UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) // When val tagSnapshot1 = userManager.getTags() @@ -191,4 +206,58 @@ class UserManagerTests : FunSpec({ verify(exactly = 1) { mockSubscriptionManager.addSmsSubscription("+15558675309") } verify(exactly = 1) { mockSubscriptionManager.removeSmsSubscription("+15558675309") } } + + test("login user with jwt calls onUserJwtInvalidated() when the jwt is unauthorized") { + // Given + val appId = "appId" + val localOneSignalId = "local-onesignalId" + val remoteOneSignalId = "remote-onesignalId" + + // mock components + val mockSubscriptionManager = mockk() + val mockIdentityModelStore = MockHelper.identityModelStore() + val mockPropertiesModelStore = MockHelper.propertiesModelStore() + val mockSubscriptionsModelStore = mockk() + val mockLanguageContext = MockHelper.languageContext() + + // mock backend service + val mockUserBackendService = mockk() + coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) + + // mock operation for login user + val mockIdentityOperationExecutor = mockk() + coEvery { mockIdentityOperationExecutor.execute(any()) } returns + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + val loginUserOperationExecutor = + LoginUserOperationExecutor( + mockIdentityOperationExecutor, + MockHelper.applicationService(), + MockHelper.deviceService(), + mockUserBackendService, + mockIdentityModelStore, + mockPropertiesModelStore, + mockSubscriptionsModelStore, + MockHelper.configModelStore(), + mockLanguageContext, + ) + val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) + + // mock user manager with jwtInvalidatedListener added + val userManager = + UserManager(mockSubscriptionManager, mockIdentityModelStore, mockPropertiesModelStore, mockLanguageContext) + mockIdentityModelStore.subscribe(userManager) + val spyJwtInvalidatedListener = spyk() + userManager.addUserJwtInvalidatedListner(spyJwtInvalidatedListener) + + // When + val response = loginUserOperationExecutor.execute(operations) + + // Then + userManager.jwtInvalidatedCallback.hasSubscribers shouldBe true + response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED + verify(exactly = 1) { mockIdentityModelStore.invalidateJwt() } + // Note: set the default value of useIdentityVerification in OneSignalImp.kt to pass the test + verify(exactly = 1) { spyJwtInvalidatedListener.onUserJwtInvalidated(any()) } + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt index 37c290855e..23f5b153e2 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt @@ -38,6 +38,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.createSubscription(any(), any(), any(), any()) } returns remoteSubscriptionId + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val subscriptionModel1 = SubscriptionModel() subscriptionModel1.id = localSubscriptionId @@ -50,6 +51,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -93,8 +95,11 @@ class SubscriptionOperationExecutorTests : FunSpec({ test("create subscription fails with retry when there is a network condition") { // Given val mockSubscriptionBackendService = mockk() - coEvery { mockSubscriptionBackendService.createSubscription(any(), any(), any(), any()) } throws BackendException(408, retryAfterSeconds = 10) + coEvery { + mockSubscriptionBackendService.createSubscription(any(), any(), any(), any()) + } throws BackendException(408, retryAfterSeconds = 10) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() @@ -103,6 +108,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -148,6 +154,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.createSubscription(any(), any(), any(), any()) } throws BackendException(404) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } answers { null } @@ -157,6 +164,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -211,6 +219,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + MockHelper.identityModelStore(), mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -241,6 +250,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ // Given val mockSubscriptionBackendService = mockk() + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val subscriptionModel1 = SubscriptionModel() subscriptionModel1.id = localSubscriptionId @@ -253,6 +263,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -285,6 +296,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.createSubscription(any(), any(), any(), any()) } returns remoteSubscriptionId + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val subscriptionModel1 = SubscriptionModel() subscriptionModel1.id = localSubscriptionId @@ -297,6 +309,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -351,6 +364,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any()) } just runs + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val subscriptionModel1 = SubscriptionModel() subscriptionModel1.id = remoteSubscriptionId @@ -364,6 +378,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -416,6 +431,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any()) } throws BackendException(408) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() @@ -424,6 +440,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -467,6 +484,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any()) } throws BackendException(404) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() @@ -475,6 +493,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -518,6 +537,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any()) } throws BackendException(404) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 } @@ -528,6 +548,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -559,6 +580,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any()) } just runs + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() every { mockSubscriptionsModelStore.remove(any(), any()) } just runs @@ -569,6 +591,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -594,6 +617,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any()) } throws BackendException(408) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() @@ -602,6 +626,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -628,6 +653,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any()) } throws BackendException(404) + val mockIdentityModelStore = MockHelper.identityModelStore() val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() @@ -636,6 +662,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + mockIdentityModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, @@ -670,6 +697,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ mockSubscriptionBackendService, MockHelper.deviceService(), AndroidMockHelper.applicationService(), + MockHelper.identityModelStore(), mockSubscriptionsModelStore, MockHelper.configModelStore(), mockBuildUserService, From 71b06ae7b8975f36ea83f927bd9d01be1a6a4292 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Mon, 12 Aug 2024 21:59:11 -0400 Subject: [PATCH 06/12] Add UI element to DemoApp to manually test JWT impl --- .../sdktest/application/MainApplication.java | 13 +- .../sdktest/model/MainActivityViewModel.java | 36 +++++- .../main/res/layout/main_activity_layout.xml | 115 +++++++++++++++--- .../app/src/main/res/values/strings.xml | 5 + 4 files changed, 151 insertions(+), 18 deletions(-) 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 3050d96fc4..b4683dbca6 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 @@ -6,9 +6,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.multidex.MultiDexApplication; - import com.onesignal.Continue; +import com.onesignal.IUserJwtInvalidatedListener; import com.onesignal.OneSignal; +import com.onesignal.UserJwtInvalidatedEvent; import com.onesignal.inAppMessages.IInAppMessageClickListener; import com.onesignal.inAppMessages.IInAppMessageClickEvent; import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent; @@ -140,6 +141,16 @@ public void onUserStateChange(@NonNull UserChangedState state) { } }); + OneSignal.addUserJwtInvalidatedListner(new IUserJwtInvalidatedListener() { + @Override + public void onUserJwtInvalidated(@NonNull UserJwtInvalidatedEvent event) { + // !!! For manual testing only + String jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxNjg4ZDhmMi1kYTdmLTQ4MTUtOGVlMy05ZDEzNzg4NDgyYzgiLCJpYXQiOjE3MTgzMDk5NzIsImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiYWxleC0wNjE0Iiwib25lc2lnbmFsX2lkIjoiYTViYjc4NDYtYzExNC00YzdkLTkzMWYtNGQ0NjhiMGE5OWJhIn0sInN1YnNjcmlwdGlvbnMiOlt7InR5cGUiOiJFbWFpbCIsInRva2VuIjoidGVzdEBkb21haW4uY29tIn0seyJpZCI6ImE2YzQxNmY3LTMxMGUtNDgzNi05Yjc4LWZiZmQ5NTgyNWNjNCJ9XX0.HsjsA2qNPwd9qov_8Px01km-dzRug-YKNNG85cMrGYI9Pdb2uoPQSdAN3Uqu7_o4pL8FRxXliYJrC52-9wH3FQ"; + OneSignal.updateUserJwt(event.getExternalId(), jwt); + Log.v(Tag.LOG_TAG, "onUserJwtInvalidated fired with ID:" + event.getExternalId()); + } + }); + 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 58069a298c..5a0177f199 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 @@ -18,6 +18,7 @@ import android.view.View; import android.view.ViewTreeObserver; import android.widget.Button; +import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Switch; @@ -83,6 +84,10 @@ public class MainActivityViewModel implements ActivityViewModel, IPushSubscripti private Button loginUserButton; private Button logoutUserButton; + // JWT + private Button invalidateJwtButton; + private Button updateJwtButton; + // Alias private TextView aliasTitleTextView; private RecyclerView aliasesRecyclerView; @@ -211,6 +216,9 @@ public ActivityViewModel onActivityCreated(Context context) { loginUserButton = getActivity().findViewById(R.id.main_activity_login_user_button); logoutUserButton = getActivity().findViewById(R.id.main_activity_logout_user_button); + invalidateJwtButton = getActivity().findViewById(R.id.main_activity_invalidate_jwt_button); + updateJwtButton = getActivity().findViewById(R.id.main_activity_update_jwt_button); + aliasTitleTextView = getActivity().findViewById(R.id.main_activity_aliases_title_text_view); noAliasesTextView = getActivity().findViewById(R.id.main_activity_aliases_no_aliases_text_view); addAliasButton = getActivity().findViewById(R.id.main_activity_add_alias_button); @@ -403,7 +411,8 @@ private void setupAppLayout() { @Override public void onSuccess(String update) { if (update != null && !update.isEmpty()) { - OneSignal.login(update); + String jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxNjg4ZDhmMi1kYTdmLTQ4MTUtOGVlMy05ZDEzNzg4NDgyYzgiLCJpYXQiOjE3MTU5NzMwNzAsImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiYWxleC0wNTE3Iiwib25lc2lnbmFsX2lkIjoiMGIzYWMyN2EtYWQ4Yi00MWVjLWJhYTYtMzI0NmNkODIyMjJkIn0sInN1YnNjcmlwdGlvbnMiOlt7InR5cGUiOiJFbWFpbCIsInRva2VuIjoiYWxleHRzYXktMDUxN0BvbmVzaWduYWwuY29tIn0seyJ0eXBlIjoiQW5kcm9pZFB1c2giLCJpZCI6ImFkMTAxY2FjLTA5MWItNDkyYy04OGJiLTgxNmZkNTNjYTBmMSJ9XX0._tlD2X8J16gDkP7__FJ8CwpqCLDwb8T14m2ugJwQvuQqbIn4b8o75cKbffbjVGcKP3YaudLCebit53aR9LTQCw"; + OneSignal.login(update, jwt); refreshState(); } } @@ -422,6 +431,7 @@ public void onFailure() { } private void setupUserLayout() { + setupJWTLayout(); setupAliasLayout(); setupEmailLayout(); setupSMSLayout(); @@ -430,6 +440,30 @@ private void setupUserLayout() { setupTriggersLayout(); } + private void setupJWTLayout() { + invalidateJwtButton.setOnClickListener(v -> { + OneSignal.updateUserJwt(OneSignal.getUser().getExternalId(), ""); + }); + updateJwtButton.setOnClickListener(v -> { + dialog.createUpdateAlertDialog("", Dialog.DialogAction.UPDATE, ProfileUtil.FieldType.JWT, new UpdateAlertDialogCallback() { + @Override + public void onSuccess(String update) { + if (update != null && !update.isEmpty()) { + OneSignal.updateUserJwt(OneSignal.getUser().getExternalId(), update); + //String jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxNjg4ZDhmMi1kYTdmLTQ4MTUtOGVlMy05ZDEzNzg4NDgyYzgiLCJpYXQiOjE3MTQwODA4MTMsImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiMjAyNDA0MjUtYWxleDQyIn0sInN1YnNjcmlwdGlvbiI6W3sidHlwZSI6IiIsImlkIjoiMmRlZGU3MzItMTEyNi00MTlkLTk5M2UtNDIzYWQyYTZiZGU5In1dfQ.rzZ-HaDm1EwxbMxd8937bWzPhPSQDDSqSzaASgZZc5A5v8g6ZQHizN9CljOmoQ4lTLm7noD6rOmR07tlZdrI5w"; + //OneSignal.login(update, jwt); + refreshState(); + } + } + + @Override + public void onFailure() { + + } + }); + }); + } + private void setupAliasLayout() { setupAliasesRecyclerView(); addAliasButton.setOnClickListener(v -> dialog.createAddPairAlertDialog("Add Alias", ProfileUtil.FieldType.ALIAS, new AddPairAlertDialogCallback() { diff --git a/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml b/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml index ee4ce93cb5..f3eb64a051 100644 --- a/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml +++ b/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml @@ -152,20 +152,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start|center_vertical" - android:gravity="start|center_vertical" - android:layout_marginBottom="4dp" android:layout_marginStart="12dp" android:layout_marginLeft="12dp" + android:layout_marginBottom="4dp" + android:gravity="start|center_vertical" android:text="@string/app" android:textColor="@color/colorDarkText" /> @@ -226,23 +226,23 @@ android:layout_width="match_parent" android:layout_height="56dp" android:layout_gravity="center" - android:gravity="center" android:layout_marginStart="12dp" android:layout_marginTop="4dp" android:layout_marginEnd="12dp" android:layout_marginBottom="12dp" - android:orientation="vertical" - android:background="@color/colorPrimary"> + android:background="@color/colorPrimary" + android:gravity="center" + android:orientation="vertical">