Skip to content
11 changes: 7 additions & 4 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
<ID>ConstructorParameterNaming:UserBackendService.kt$UserBackendService$private val _httpClient: IHttpClient</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _customEventController: ICustomEventController</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _identityModelStore: IdentityModelStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _jwtTokenStore: JwtTokenStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _languageContext: ILanguageContext</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _propertiesModelStore: PropertiesModelStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _subscriptionManager: ISubscriptionManager</ID>
Expand All @@ -173,7 +174,6 @@
<ID>ForbiddenComment:HttpClient.kt$HttpClient$// TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT?</ID>
<ID>ForbiddenComment:IPreferencesService.kt$PreferenceOneSignalKeys$* (String) The serialized IAMs TODO: This isn't currently used, determine if actually needed for cold start IAM fetch delay</ID>
<ID>ForbiddenComment:IUserBackendService.kt$IUserBackendService$// TODO: Change to send only the push subscription, optimally</ID>
<ID>ForbiddenComment:LoginHelper.kt$LoginHelper$// TODO: Set JWT Token for all future requests.</ID>
<ID>ForbiddenComment:LogoutHelper.kt$LogoutHelper$// TODO: remove JWT Token for all future requests.</ID>
<ID>ForbiddenComment:ParamsBackendService.kt$ParamsBackendService$// TODO: New</ID>
<ID>ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO after we remove IAM from being an activity window we may be able to remove this handler</ID>
Expand Down Expand Up @@ -248,6 +248,7 @@
<ID>MagicNumber:OSDatabase.kt$OSDatabase$8</ID>
<ID>MagicNumber:OSDatabase.kt$OSDatabase$9</ID>
<ID>MagicNumber:OneSignalDispatchers.kt$OneSignalDispatchers$1024</ID>
<ID>MagicNumber:OneSignalImp.kt$OneSignalImp$8</ID>
<ID>MagicNumber:OperationRepo.kt$OperationRepo$1_000</ID>
<ID>MagicNumber:OutcomeEventsController.kt$OutcomeEventsController$1000</ID>
<ID>MagicNumber:PermissionsActivity.kt$PermissionsActivity$23</ID>
Expand Down Expand Up @@ -412,7 +413,7 @@
<ID>TooManyFunctions:OutcomeEventsController.kt$OutcomeEventsController : IOutcomeEventsControllerIStartableServiceISessionLifecycleHandler</ID>
<ID>TooManyFunctions:PreferencesService.kt$PreferencesService : IPreferencesServiceIStartableService</ID>
<ID>TooManyFunctions:SubscriptionManager.kt$SubscriptionManager : ISubscriptionManagerIModelStoreChangeHandlerISessionLifecycleHandler</ID>
<ID>TooManyFunctions:UserManager.kt$UserManager : IUserManagerISingletonModelStoreChangeHandler</ID>
<ID>TooManyFunctions:UserManager.kt$UserManager : IUserManagerISingletonModelStoreChangeHandlerIJwtUpdateListener</ID>
<ID>UndocumentedPublicClass:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$Callback</ID>
<ID>UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils</ID>
<ID>UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils$SchemaType</ID>
Expand Down Expand Up @@ -457,7 +458,6 @@
<ID>UndocumentedPublicClass:JSONConverter.kt$JSONConverter</ID>
<ID>UndocumentedPublicClass:JSONUtils.kt$JSONUtils</ID>
<ID>UndocumentedPublicClass:Logging.kt$Logging</ID>
<ID>UndocumentedPublicClass:LoginHelper.kt$LoginHelper</ID>
<ID>UndocumentedPublicClass:LogoutHelper.kt$LogoutHelper</ID>
<ID>UndocumentedPublicClass:MigrationRecovery.kt$MigrationRecovery : IMigrationRecovery</ID>
<ID>UndocumentedPublicClass:NetworkUtils.kt$NetworkUtils</ID>
Expand Down Expand Up @@ -626,13 +626,16 @@
<ID>UnusedPrivateMember:AndroidUtils.kt$AndroidUtils$var requestPermission: String? = null</ID>
<ID>UnusedPrivateMember:ApplicationService.kt$ApplicationService$val listenerKey = "decorViewReady:$runnable"</ID>
<ID>UnusedPrivateMember:JSONUtils.kt$JSONUtils$`object`: Any</ID>
<ID>UnusedPrivateMember:LoginHelper.kt$LoginHelper$jwtBearerToken: String? = null</ID>
<ID>UnusedPrivateMember:OSDatabase.kt$OSDatabase.Companion$private const val FLOAT_TYPE = " FLOAT"</ID>
<ID>UnusedPrivateMember:OperationRepo.kt$OperationRepo$private val _time: ITime</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'login'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'logout'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'updateUserJwt'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'addUserJwtInvalidatedListener'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'login'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'logout'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'removeUserJwtInvalidatedListener'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'updateUserJwt'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before use")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.")</ID>
</CurrentIssues>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ interface IOneSignal {
*/
fun logout()

/**
* Update the JWT bearer token associated with [externalId]. Use this when your backend
* has issued a new JWT for an already-logged-in user (e.g. in response to a previous
* [IUserJwtInvalidatedListener.onUserJwtInvalidated] callback). Stores the JWT and
* wakes the operation queue so any deferred ops can dispatch with the fresh token.
*
* @param externalId The external ID the JWT belongs to.
* @param token The new JWT bearer token issued by your backend.
*/
fun updateUserJwt(
externalId: String,
token: String,
)

/**
* Subscribe a listener for JWT-invalidated events. Fires on a background thread when
* the SDK detects that the stored JWT for a user is no longer valid (typically after
* a 401 from the OneSignal backend). Apps should respond by fetching a fresh JWT from
* their backend and supplying it via [updateUserJwt].
*
* Pure pub/sub: only listeners subscribed at the time of the invalidation receive the
* event. Subscribe early (e.g. in `Application.onCreate`) to avoid missing events.
*/
Comment thread
claude[bot] marked this conversation as resolved.
fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener)

/** Unsubscribe a listener previously registered via [addUserJwtInvalidatedListener]. */
fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener)

Comment thread
claude[bot] marked this conversation as resolved.
// Suspend versions of property accessors and methods to avoid blocking threads

/**
Expand Down Expand Up @@ -226,4 +254,16 @@ interface IOneSignal {
* Logout the current user (suspend version).
*/
suspend fun logoutSuspend()

/**
* Update the JWT bearer token associated with [externalId] (suspend version). Suspends
* until SDK initialization is complete, then stores the JWT and wakes the operation queue.
*
* @param externalId The external ID the JWT belongs to.
* @param token The new JWT bearer token issued by your backend.
*/
suspend fun updateUserJwtSuspend(
externalId: String,
token: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.onesignal

/**
* Implement this interface and provide an instance to
* [IOneSignal.addUserJwtInvalidatedListener] to be notified when the SDK has
* detected that the JWT for a user is no longer valid (typically a 401 from
* the OneSignal backend on a request signed with that JWT).
*
* Threading: delivered on a background dispatcher
* (`OneSignalDispatchers.launchOnDefault`). Implementations should not assume a
* specific thread and should re-dispatch to the UI thread if needed.
*
* Pure pub/sub: only listeners subscribed at the time of the invalidation
* receive the event. Subscribe early (e.g. in `Application.onCreate`) to avoid
* missing cold-start 401s.
*/
fun interface IUserJwtInvalidatedListener {
/**
* Called when the JWT is invalidated for [UserJwtInvalidatedEvent.externalId].
* Apps should use this signal to fetch a fresh JWT from their backend and
* supply it via [IOneSignal.updateUserJwt].
*
* @param event Describes which user's JWT was invalidated.
*/
fun onUserJwtInvalidated(event: UserJwtInvalidatedEvent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,39 @@ object OneSignal {
@JvmStatic
fun logout() = oneSignal.logout()

/**
* Update the JWT bearer token associated with [externalId]. Use this when your backend
* has issued a new JWT for an already-logged-in user (e.g. in response to a previous
* [IUserJwtInvalidatedListener.onUserJwtInvalidated] callback). Stores the JWT and
* wakes the operation queue so any deferred ops can dispatch with the fresh token.
*
* @param externalId The external ID the JWT belongs to.
* @param token The new JWT bearer token issued by your backend.
*/
@JvmStatic
fun updateUserJwt(
externalId: String,
token: String,
) = oneSignal.updateUserJwt(externalId, token)

/**
* Subscribe a listener for JWT-invalidated events. Fires on a background thread when
* the SDK detects that the stored JWT for a user is no longer valid (typically after
* a 401 from the OneSignal backend). Apps should respond by fetching a fresh JWT from
* their backend and supplying it via [updateUserJwt].
*
* Pure pub/sub: only listeners subscribed at the time of the invalidation receive the
* event. Subscribe early (e.g. in `Application.onCreate`) to avoid missing events.
*/
@JvmStatic
fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) =
oneSignal.addUserJwtInvalidatedListener(listener)

/** Unsubscribe a listener previously registered via [addUserJwtInvalidatedListener]. */
@JvmStatic
fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) =
oneSignal.removeUserJwtInvalidatedListener(listener)

private val oneSignal: IOneSignal by lazy {
OneSignalImp()
}
Expand Down Expand Up @@ -405,6 +438,21 @@ object OneSignal {
oneSignal.logoutSuspend()
}

/**
* Update the JWT bearer token associated with [externalId] without blocking the calling
* thread. Suspend-safe version of [updateUserJwt].
*
* @param externalId The external ID the JWT belongs to.
* @param token The new JWT bearer token issued by your backend.
*/
@JvmStatic
suspend fun updateUserJwtSuspend(
externalId: String,
token: String,
) {
oneSignal.updateUserJwtSuspend(externalId, token)
}

/**
* Used to retrieve services from the SDK when constructor dependency injection is not an
* option.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.onesignal

/**
* The event passed into [IUserJwtInvalidatedListener.onUserJwtInvalidated].
* Delivery occurs on a background thread.
*/
class UserJwtInvalidatedEvent(
val externalId: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ internal fun OperationRepo.hasValidJwtIfRequired(

/**
* Handles a [com.onesignal.core.internal.operations.ExecutionResult.FAIL_UNAUTHORIZED] response
* when IV behavior is active. Invalidates the JWT for the failing op's externalId (which fires
* `IJwtUpdateListener.onJwtInvalidated` to subscribers, surfacing to the developer via the
* public-API layer), and re-queues the ops (waiter wake with `false` so `enqueueAndWait`
* when IV behavior is active. Invalidates the JWT for the failing op's externalId
* and re-queues the ops (waiter wake with `false` so `enqueueAndWait`
* callers don't hang).
*
* Returns `true` if IV-specific handling was applied (caller should stop processing this result),
Expand All @@ -48,8 +47,10 @@ internal fun OperationRepo.handleFailUnauthorized(
if (!ivBehaviorActive) return false
val externalId = startingOp.operation.externalId ?: return false

// Fires onJwtInvalidated to subscribers BEFORE we wake waiters — otherwise an
// `enqueueAndWait` caller could return before the developer-facing event propagates.
// Schedules an async fire of onUserJwtInvalidated to subscribers via
// OneSignalDispatchers.launchOnDefault — the developer-facing listener invocation is
// NOT ordered with respect to the waiter.wake below; awaiting `enqueueAndWait` callers
// may resume before, after, or concurrent with the listener.
jwtTokenStore.invalidateJwt(externalId)
Logging.info(
"Operation execution failed with 401 Unauthorized, JWT invalidated for user: $externalId. " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.onesignal.internal

import android.content.Context
import com.onesignal.IOneSignal
import com.onesignal.IUserJwtInvalidatedListener
import com.onesignal.common.AndroidUtils
import com.onesignal.common.DeviceUtils
import com.onesignal.common.OneSignalUtils
Expand Down Expand Up @@ -37,6 +38,7 @@ import com.onesignal.user.internal.LoginHelper
import com.onesignal.user.internal.LogoutHelper
import com.onesignal.user.internal.UserSwitcher
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.properties.PropertiesModelStore
import com.onesignal.user.internal.resolveAppId
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
Expand Down Expand Up @@ -142,6 +144,7 @@ internal class OneSignalImp(
private val propertiesModelStore: PropertiesModelStore by lazy { services.getService<PropertiesModelStore>() }
private val subscriptionModelStore: SubscriptionModelStore by lazy { services.getService<SubscriptionModelStore>() }
private val preferencesService: IPreferencesService by lazy { services.getService<IPreferencesService>() }
private val jwtTokenStore: JwtTokenStore by lazy { services.getService<JwtTokenStore>() }
private val listOfModules =
listOf(
"com.onesignal.notifications.NotificationsModule",
Expand Down Expand Up @@ -220,6 +223,7 @@ internal class OneSignalImp(
userSwitcher = userSwitcher,
operationRepo = operationRepo,
configModel = configModel,
jwtTokenStore = jwtTokenStore,
lock = loginLogoutLock,
)
}
Expand Down Expand Up @@ -381,7 +385,7 @@ internal class OneSignalImp(
externalId: String,
jwtBearerToken: String?,
) {
Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")
Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})")

if (isBackgroundThreadingEnabled) {
waitForInit(operationName = "login")
Expand Down Expand Up @@ -428,6 +432,47 @@ internal class OneSignalImp(
}
}

override fun updateUserJwt(
externalId: String,
Comment thread
claude[bot] marked this conversation as resolved.
token: String,
) {
Logging.log(LogLevel.DEBUG, "updateUserJwt(externalId: $externalId, token: ...${token.takeLast(8)})")

if (isBackgroundThreadingEnabled) {
waitForInit(operationName = "updateUserJwt")
} else {
if (!isInitialized) {
throw IllegalStateException("Must call 'initWithContext' before 'updateUserJwt'")
}
}

jwtTokenStore.putJwt(externalId, token)
// Wake the queue so any deferred ops can dispatch with the fresh token.
operationRepo.forceExecuteOperations()
}

override fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
if (isBackgroundThreadingEnabled) {
waitForInit(operationName = "addUserJwtInvalidatedListener")
} else {
if (!isInitialized) {
throw IllegalStateException("Must call 'initWithContext' before 'addUserJwtInvalidatedListener'")
}
}
jwtTokenStore.addUserJwtInvalidatedListener(listener)
}

override fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
if (isBackgroundThreadingEnabled) {
waitForInit(operationName = "removeUserJwtInvalidatedListener")
} else {
if (!isInitialized) {
throw IllegalStateException("Must call 'initWithContext' before 'removeUserJwtInvalidatedListener'")
}
}
jwtTokenStore.removeUserJwtInvalidatedListener(listener)
}
Comment thread
claude[bot] marked this conversation as resolved.

override fun <T> hasService(c: Class<T>): Boolean = services.hasService(c)

override fun <T> getService(c: Class<T>): T = services.getService(c)
Expand Down Expand Up @@ -657,7 +702,7 @@ internal class OneSignalImp(
externalId: String,
jwtBearerToken: String?,
) = withContext(runtimeIoDispatcher) {
Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")
Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})")

suspendUntilInit(operationName = "login")

Expand All @@ -669,6 +714,22 @@ internal class OneSignalImp(
loginHelper.enqueueLogin(context)
}

override suspend fun updateUserJwtSuspend(
externalId: String,
token: String,
) = withContext(runtimeIoDispatcher) {
Logging.log(LogLevel.DEBUG, "updateUserJwtSuspend(externalId: $externalId, token: ...${token.takeLast(8)})")

suspendUntilInit(operationName = "updateUserJwt")

if (!isInitialized) {
throw IllegalStateException("'initWithContext failed' before 'updateUserJwt'")
}

jwtTokenStore.putJwt(externalId, token)
operationRepo.forceExecuteOperations()
}

override suspend fun logoutSuspend() =
withContext(runtimeIoDispatcher) {
Logging.log(LogLevel.DEBUG, "logoutSuspend()")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ internal class UserModule : IModule {
builder.register<LoginUserOperationExecutor>().provides<IOperationExecutor>()
builder.register<LoginUserFromSubscriptionOperationExecutor>().provides<IOperationExecutor>()
builder.register<RefreshUserOperationExecutor>().provides<IOperationExecutor>()
builder.register<UserManager>().provides<IUserManager>()
builder.register<UserManager>()
.provides<IUserManager>()
.provides<UserManager>()
Comment thread
claude[bot] marked this conversation as resolved.
builder.register<CustomEventController>().provides<ICustomEventController>()
builder.register<CustomEventOperationExecutor>().provides<IOperationExecutor>()
builder.register<CustomEventBackendService>().provides<ICustomEventBackendService>()
Expand Down
Loading
Loading