Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,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:OperationRepo.kt$OperationRepo$// TODO: Need to provide callback for app to reset JWT. For now, fail with no retry.</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>
<ID>ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO improve this method</ID>
<ID>ForbiddenComment:PermissionsViewModel.kt$PermissionsViewModel.Companion$// TODO this will be removed once the handler is deleted</ID>
Expand Down Expand Up @@ -217,7 +213,7 @@
<ID>LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )</ID>
<ID>LongParameterList:IParamsBackendService.kt$ParamsObject$( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, var notificationChannels: JSONArray? = null, var firebaseAnalytics: Boolean? = null, var restoreTTLFilter: Boolean? = null, var clearGroupOnSummaryClick: Boolean? = null, var receiveReceiptEnabled: Boolean? = null, var disableGMSMissingPrompt: Boolean? = null, var unsubscribeWhenNotificationsDisabled: Boolean? = null, var locationShared: Boolean? = null, var requiresUserPrivacyConsent: Boolean? = null, var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, )</ID>
<ID>LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, jwt: String? = null, )</ID>
<ID>LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, )</ID>
<ID>LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, private val _consistencyManager: IConsistencyManager, )</ID>
<ID>LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, )</ID>
<ID>LongParameterList:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _jwtTokenStore: JwtTokenStore, )</ID>
<ID>LongParameterList:SubscriptionObject.kt$SubscriptionObject$( val id: String? = null, val type: SubscriptionObjectType? = null, val token: String? = null, val enabled: Boolean? = null, val notificationTypes: Int? = null, val sdk: String? = null, val deviceModel: String? = null, val deviceOS: String? = null, val rooted: Boolean? = null, val netType: Int? = null, val carrier: String? = null, val appVersion: String? = null, )</ID>
Expand Down
8 changes: 4 additions & 4 deletions OneSignalSDK/detekt/detekt-baseline-in-app-messages.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<ID>LongMethod:InAppRepository.kt$InAppRepository$override suspend fun cleanCachedInAppMessages()</ID>
<ID>LongParameterList:IInAppBackendService.kt$IInAppBackendService$( appId: String, subscriptionId: String, variantId: String?, messageId: String, clickId: String?, isFirstClick: Boolean, )</ID>
<ID>LongParameterList:InAppDisplayer.kt$InAppDisplayer$( private val _applicationService: IApplicationService, private val _lifecycle: IInAppLifecycleService, private val _promptFactory: IInAppMessagePromptFactory, private val _backend: IInAppBackendService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _time: ITime, )</ID>
<ID>LongParameterList:InAppMessagesManager.kt$InAppMessagesManager$( private val _applicationService: IApplicationService, private val _sessionService: ISessionService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _userManager: IUserManager, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _outcomeEventsController: IOutcomeEventsController, private val _state: InAppStateService, private val _prefs: IInAppPreferencesController, private val _repository: IInAppRepository, private val _backend: IInAppBackendService, private val _triggerController: ITriggerController, private val _triggerModelStore: TriggerModelStore, private val _displayer: IInAppDisplayer, private val _lifecycle: IInAppLifecycleService, private val _languageContext: ILanguageContext, private val _time: ITime, private val _consistencyManager: IConsistencyManager, )</ID>
<ID>LongParameterList:InAppMessagesManager.kt$InAppMessagesManager$( private val _applicationService: IApplicationService, private val _sessionService: ISessionService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _userManager: IUserManager, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _outcomeEventsController: IOutcomeEventsController, private val _state: InAppStateService, private val _prefs: IInAppPreferencesController, private val _repository: IInAppRepository, private val _backend: IInAppBackendService, private val _triggerController: ITriggerController, private val _triggerModelStore: TriggerModelStore, private val _displayer: IInAppDisplayer, private val _lifecycle: IInAppLifecycleService, private val _languageContext: ILanguageContext, private val _time: ITime, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, )</ID>
<ID>LongParameterList:OneSignalAnimate.kt$OneSignalAnimate$( view: View, deltaFromY: Float, deltaToY: Float, duration: Int, interpolator: Interpolator?, animCallback: Animation.AnimationListener?, )</ID>
<ID>MagicNumber:DraggableRelativeLayout.kt$DraggableRelativeLayout$3</ID>
<ID>MagicNumber:DraggableRelativeLayout.kt$DraggableRelativeLayout$3000</ID>
Expand Down Expand Up @@ -103,12 +103,12 @@
<ID>ReturnCount:DraggableRelativeLayout.kt$DraggableRelativeLayout.&lt;no name provided&gt;$override fun clampViewPositionVertical( child: View, top: Int, dy: Int, ): Int</ID>
<ID>ReturnCount:DynamicTriggerController.kt$DynamicTriggerController$fun dynamicTriggerShouldFire(trigger: Trigger): Boolean</ID>
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$override suspend fun getIAMData( appId: String, messageId: String, variantId: String?, ): GetIAMDataResponse</ID>
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$private suspend fun attemptFetchWithRetries( baseUrl: String, rywData: RywData, sessionDurationProvider: () -&gt; Long, ): List&lt;InAppMessage&gt;?</ID>
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$private suspend fun attemptFetchWithRetries( baseUrl: String, rywData: RywData, sessionDurationProvider: () -&gt; Long, jwt: String? = null, ): List&lt;InAppMessage&gt;?</ID>
<ID>ReturnCount:InAppHydrator.kt$InAppHydrator$fun hydrateIAMMessageContent(jsonObject: JSONObject): InAppMessageContent?</ID>
<ID>ReturnCount:InAppMessage.kt$InAppMessage$private fun parseEndTimeJson(json: JSONObject): Date?</ID>
<ID>ReturnCount:InAppMessagePreviewHandler.kt$InAppMessagePreviewHandler$private fun inAppPreviewPushUUID(payload: JSONObject): String?</ID>
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$override fun onMessageWasDisplayed(message: InAppMessage)</ID>
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$private suspend fun fetchMessages(rywData: RywData)</ID>
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$private suspend fun fetchMessages(rywData: RywData?)</ID>
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun evaluateMessageTriggers(message: InAppMessage): Boolean</ID>
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun isTriggerOnMessage( message: InAppMessage, triggersKeys: Collection&lt;String&gt;, ): Boolean</ID>
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun messageHasOnlyDynamicTriggers(message: InAppMessage): Boolean</ID>
Expand All @@ -124,7 +124,7 @@
<ID>TooManyFunctions:InAppBackendService.kt$InAppBackendService : IInAppBackendService</ID>
<ID>TooManyFunctions:InAppMessage.kt$InAppMessage : IInAppMessage</ID>
<ID>TooManyFunctions:InAppMessageView.kt$InAppMessageView</ID>
<ID>TooManyFunctions:InAppMessagesManager.kt$InAppMessagesManager : IInAppMessagesManagerIStartableServiceISubscriptionChangedHandlerISingletonModelStoreChangeHandlerIInAppLifecycleEventHandlerITriggerHandlerISessionLifecycleHandlerIApplicationLifecycleHandler</ID>
<ID>TooManyFunctions:InAppMessagesManager.kt$InAppMessagesManager : IInAppMessagesManagerIStartableServiceISubscriptionChangedHandlerISingletonModelStoreChangeHandlerIInAppLifecycleEventHandlerITriggerHandlerISessionLifecycleHandlerIApplicationLifecycleHandlerIJwtUpdateListener</ID>
<ID>TooManyFunctions:TriggerController.kt$TriggerController : ITriggerControllerIModelStoreChangeHandler</ID>
<ID>TooManyFunctions:WebViewManager.kt$WebViewManager : IActivityLifecycleHandler</ID>
<ID>UndocumentedPublicClass:TriggerModel.kt$TriggerModel : Model</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,21 @@ class ConsistencyManager : IConsistencyManager {
}

override suspend fun resolveConditionsWithID(id: String) {
val completedConditions = mutableListOf<Pair<ICondition, CompletableDeferred<RywData?>>>()
mutex.withLock {
val completedConditions = mutableListOf<Pair<ICondition, CompletableDeferred<RywData?>>>()

for ((condition, deferred) in conditions) {
if (condition.id == id) {
if (!deferred.isCompleted) {
deferred.complete(null)
for ((condition, deferred) in conditions) {
if (condition.id == id) {
if (!deferred.isCompleted) {
deferred.complete(null)
}
completedConditions.add(Pair(condition, deferred))
}
}
completedConditions.add(Pair(condition, deferred))
}

// Remove completed conditions from the list
conditions.removeAll(completedConditions)
// Remove completed conditions from the list
conditions.removeAll(completedConditions)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.onesignal.core.internal.config.impl
import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.common.modeling.ModelChangedArgs
import com.onesignal.common.threading.suspendifyOnIO
import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.operations.IOperationRepo
Expand Down Expand Up @@ -42,21 +43,25 @@ internal class IdentityVerificationService(

val useIV = model.useIdentityVerification

var jwtInvalidatedExternalId: String? = null
if (useIV == true) {
Logging.debug("IdentityVerificationService: IV enabled, purging anonymous operations")
_operationRepo.removeOperationsWithoutExternalId()
suspendifyOnIO {
_operationRepo.awaitInitialized()

val externalId = _identityModelStore.model.externalId
if (externalId != null && _jwtTokenStore.getJwt(externalId) == null) {
Logging.debug("IdentityVerificationService: IV enabled but no JWT for $externalId, will fire invalidated event after queue wake")
jwtInvalidatedExternalId = externalId
var jwtInvalidatedExternalId: String? = null
if (useIV == true) {
Logging.debug("IdentityVerificationService: IV enabled, purging anonymous operations")
_operationRepo.removeOperationsWithoutExternalId()

val externalId = _identityModelStore.model.externalId
if (externalId != null && _jwtTokenStore.getJwt(externalId) == null) {
Logging.debug("IdentityVerificationService: IV enabled but no JWT for $externalId, will fire invalidated event after queue wake")
jwtInvalidatedExternalId = externalId
}
}
}

_operationRepo.forceExecuteOperations()
_operationRepo.forceExecuteOperations()

jwtInvalidatedExternalId?.let { _userManager.fireJwtInvalidated(it) }
jwtInvalidatedExternalId?.let { _userManager.fireJwtInvalidated(it) }
}
}

override fun onModelUpdated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,7 @@ internal class OperationRepo(
_operationModelStore.remove(it.operation.id)
it.waiter?.wake(false)
}
if (toRemove.isNotEmpty()) {
Logging.debug("OperationRepo: removed ${toRemove.size} anonymous operations (no externalId)")
}
Logging.debug("OperationRepo: removeOperationsWithoutExternalId removed ${toRemove.size} of ${toRemove.size + queue.size} operations")

// IV=ON never transfers anonymous state; clear existingOnesignalId so
// the executor takes the createUser (upsert) path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,45 @@ internal open class UserManager(
private val jwtInvalidatedAppCallbackScope =
CoroutineScope(SupervisorJob() + Dispatchers.Default)

private val jwtInvalidatedLock = Any()
private var pendingJwtInvalidatedExternalId: String? = null

fun addJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
jwtInvalidatedNotifier.subscribe(listener)
val pendingExternalId: String?
synchronized(jwtInvalidatedLock) {
jwtInvalidatedNotifier.subscribe(listener)
pendingExternalId = pendingJwtInvalidatedExternalId
pendingJwtInvalidatedExternalId = null
}
pendingExternalId?.let {
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(it))
}
}
Comment on lines 61 to 71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In addJwtInvalidatedListener, the listener is subscribed (making hasSubscribers=true) inside jwtInvalidatedLock, but the pending-replay delivery happens synchronously outside the lock, creating two problems. First, a concurrent fireJwtInvalidated call in that narrow window will also fire to the newly-registered listener, so the developer's onUserJwtInvalidated may be invoked twice in rapid succession. Second, the replay runs synchronously on the caller's thread rather than through jwtInvalidatedAppCallbackScope.launch (Dispatchers.Default), violating the async delivery contract documented on fireJwtInvalidated and missing the runCatching protection present on the normal path. Fix: schedule the pending replay via jwtInvalidatedAppCallbackScope.launch { runCatching { ... } } inside the synchronized block, matching the documented asynchronous contract.

Extended reasoning...

What the bugs are and how they manifest

addJwtInvalidatedListener (UserManager.kt lines 61–71) has two related defects introduced in the pending-replay logic.

Bug 1 — double-delivery race: The listener is subscribed to jwtInvalidatedNotifier inside jwtInvalidatedLock (which makes hasSubscribers == true visible to other threads immediately). The lock is then released, and only after that release does the method call listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(it)) directly. In the window between lock release and that direct call, a concurrent thread running fireJwtInvalidated(externalId) can acquire the lock, observe hasSubscribers == true, and schedule a coroutine (via jwtInvalidatedAppCallbackScope.launch) that also fires to every subscriber. When that coroutine executes it calls the same listener for the same (or coincident) externalId, resulting in two onUserJwtInvalidated invocations in quick succession.

Bug 2 — synchronous replay on caller thread: The fireJwtInvalidated method is documented as delivering events "asynchronously so the caller … is not blocked by developer code" and uses jwtInvalidatedAppCallbackScope.launch throughout. The replay path at lines 68–70 is synchronous and unprotected by runCatching. If a developer registers the listener on the main thread and a pending event exists, the callback fires synchronously on the main thread before addJwtInvalidatedListener returns — and any exception from developer code propagates up to the registration call, unlike the normal path that catches and logs it.

Specific code path that triggers it

Between (A)'s lock release and (B)'s synchronous call, step (C) can observe hasSubscribers == true and schedule an async coroutine. Both (B) and the coroutine then deliver to the same listener.

Addressing the refutation

A refutation argues that the concurrent fireJwtInvalidated in the race window represents a new 401 event rather than a re-delivery of the buffered startup event, so two callbacks are technically correct. This is valid for the case where a genuine second 401 fires while the listener is being registered — those truly are separate events. However, the race is indistinguishable to the developer (both deliveries carry the same externalId), and the probability of a genuine concurrent 401 during the few-instruction window is low enough that in practice developers will treat this as a spurious duplicate. More importantly, both bugs are independently present regardless of this race: Bug 2 (synchronous delivery + missing runCatching) exists even without any concurrent thread.

Why existing code doesn't prevent it

EventProducer.subscribe uses the synchronized list's own internal lock, separate from jwtInvalidatedLock. Once the listener is added inside jwtInvalidatedLock, it becomes visible to jwtInvalidatedNotifier.hasSubscribers on other threads immediately, before the outer lock is released. There is no mechanism to suppress concurrent fireJwtInvalidated calls in the window between the lock release and the synchronous delivery.

Impact

A developer's onUserJwtInvalidated callback called twice causes duplicate token-refresh requests, double-posted analytics, or unexpected state-machine transitions. The synchronous delivery can cause an ANR if the callback does I/O and is called on the main thread. An unguarded developer exception in the replay path crashes or corrupts state at the registration call site instead of being logged and swallowed.

Step-by-step proof (double delivery)

  1. SDK starts; no listeners. fireJwtInvalidated('alice') is called → buffered: pendingJwtInvalidatedExternalId = 'alice'.
  2. Developer calls addJwtInvalidatedListener(listener). Lock acquired; listener subscribed (hasSubscribers=true); pendingExternalId='alice' captured; pending cleared; lock released.
  3. OperationRepo processes a 401 and calls fireJwtInvalidated('alice'). Lock acquired; hasSubscribers=true → launches coroutine to fire 'alice' to all listeners. Lock released.
  4. addJwtInvalidatedListener delivers 'alice' synchronously to listener (step B above).
  5. The coroutine from step 3 executes and fires 'alice' to all listeners, including our listener.
  6. listener.onUserJwtInvalidated('alice') has been called twice.

How to fix

Schedule the replay via the same async path used for normal delivery, inside the lock, before releasing it:

Scheduling the coroutine inside the lock ensures that any concurrent fireJwtInvalidated that acquires the lock afterwards will see the pending state already cleared, and its own coroutine will be the only remaining delivery of that event.


fun removeJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
jwtInvalidatedNotifier.unsubscribe(listener)
}

/**
* Schedules [IUserJwtInvalidatedListener] delivery on a background dispatcher so HYDRATE and
* operation-repo paths can finish internal work before app code runs.
* Fires [IUserJwtInvalidatedListener] to all subscribers asynchronously so the caller
* (e.g. OperationRepo) is not blocked by developer code. If no listeners are registered yet
* (e.g. during SDK init), stores the externalId so it can be replayed when the first
* listener is added via [addJwtInvalidatedListener].
*/
fun fireJwtInvalidated(externalId: String) {
jwtInvalidatedAppCallbackScope.launch {
runCatching {
jwtInvalidatedNotifier.fire { listener ->
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(externalId))
synchronized(jwtInvalidatedLock) {
if (jwtInvalidatedNotifier.hasSubscribers) {
jwtInvalidatedAppCallbackScope.launch {
runCatching {
jwtInvalidatedNotifier.fire { listener ->
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(externalId))
}
}.onFailure {
Logging.warn("Failed to deliver JWT invalidated event for externalId=$externalId", it)
}
}
}.onFailure {
Logging.warn("Failed to deliver JWT invalidated event for externalId=$externalId", it)
} else {
pendingJwtInvalidatedExternalId = externalId
}
}
}
Expand Down Expand Up @@ -297,7 +316,11 @@ internal open class UserManager(
override fun onModelReplaced(
model: IdentityModel,
tag: String,
) { }
) {
synchronized(jwtInvalidatedLock) {
pendingJwtInvalidatedExternalId = null
}
}

override fun onModelUpdated(
args: ModelChangedArgs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@ class CreateUserResponse(
* The subscriptions for the user.
*/
val subscriptions: List<SubscriptionObject>,
/**
* Read-your-write consistency data returned by the backend, if any.
*/
val rywData: RywData? = null,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.onesignal.user.internal.backend.impl

import com.onesignal.common.consistency.RywData
import com.onesignal.common.expandJSONArray
import com.onesignal.common.putJSONArray
import com.onesignal.common.putMap
Expand All @@ -8,6 +9,7 @@ import com.onesignal.common.safeBool
import com.onesignal.common.safeDouble
import com.onesignal.common.safeInt
import com.onesignal.common.safeJSONObject
import com.onesignal.common.safeLong
import com.onesignal.common.safeString
import com.onesignal.common.toMap
import com.onesignal.user.internal.backend.CreateUserResponse
Expand Down Expand Up @@ -55,7 +57,11 @@ object JSONConverter {
return@expandJSONArray null
}

return CreateUserResponse(respIdentities, respProperties, respSubscriptions)
val rywToken = jsonObject.safeString("ryw_token")
val rywDelay = jsonObject.safeLong("ryw_delay")
val rywData = if (rywToken != null) RywData(rywToken, rywDelay) else null

return CreateUserResponse(respIdentities, respProperties, respSubscriptions, rywData)
}

fun convertToJSON(properties: PropertiesObject): JSONObject {
Expand Down
Loading
Loading