diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml
index 98692b29ce..69b24399f0 100644
--- a/OneSignalSDK/detekt/detekt-baseline-core.xml
+++ b/OneSignalSDK/detekt/detekt-baseline-core.xml
@@ -36,6 +36,8 @@
ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _buildUserService: IRebuildUserService
ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityBackend: IIdentityBackendService
ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityModelStore: IdentityModelStore
+ ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityVerificationService: IdentityVerificationService
+ ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _jwtTokenStore: JwtTokenStore
ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _newRecordState: NewRecordsState
ConstructorParameterNaming:InfluenceDataRepository.kt$InfluenceDataRepository$private val _configModelStore: ConfigModelStore
ConstructorParameterNaming:InfluenceManager.kt$InfluenceManager$private val _applicationService: IApplicationService
@@ -45,6 +47,7 @@
ConstructorParameterNaming:JwtTokenStore.kt$JwtTokenStore$private val _prefs: IPreferencesService
ConstructorParameterNaming:LanguageContext.kt$LanguageContext$private val _propertiesModelStore: PropertiesModelStore
ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _identityModelStore: IdentityModelStore
+ ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _identityVerificationService: IdentityVerificationService
ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _propertiesModelStore: PropertiesModelStore
ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _application: IApplicationService
@@ -52,6 +55,8 @@
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _deviceService: IDeviceService
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityModelStore: IdentityModelStore
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityOperationExecutor: IdentityOperationExecutor
+ ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService
+ ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _languageContext: ILanguageContext
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore
ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _subscriptionsModelStore: SubscriptionModelStore
@@ -97,6 +102,8 @@
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _buildUserService: IRebuildUserService
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _configModelStore: ConfigModelStore
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _identityModelStore: IdentityModelStore
+ ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService
+ ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _newRecordState: NewRecordsState
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore
ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _subscriptionsModelStore: SubscriptionModelStore
@@ -127,6 +134,8 @@
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _configModelStore: ConfigModelStore
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _consistencyManager: IConsistencyManager
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _deviceService: IDeviceService
+ ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _identityVerificationService: IdentityVerificationService
+ ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _jwtTokenStore: JwtTokenStore
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _newRecordState: NewRecordsState
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService
ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _subscriptionModelStore: SubscriptionModelStore
@@ -138,6 +147,8 @@
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _buildUserService: IRebuildUserService
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _consistencyManager: IConsistencyManager
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _identityModelStore: IdentityModelStore
+ ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService
+ ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _newRecordState: NewRecordsState
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore
ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _userBackend: IUserBackendService
@@ -177,6 +188,7 @@
InstanceOfCheckForException:HttpClient.kt$HttpClient$t is UnknownHostException
LongMethod:ApplicationService.kt$ApplicationService$override suspend fun waitUntilSystemConditionsAvailable(): Boolean
LongMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams()
+ LongMethod:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome
LongMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse
LongMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
LongMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse
@@ -197,16 +209,17 @@
LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun sendPurchases( skusToAdd: ArrayList<String>, newPurchaseTokens: ArrayList<String>, )
LongMethod:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
LongParameterList:CreateSubscriptionOperation.kt$CreateSubscriptionOperation$(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus)
- LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, )
+ LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, jwt: String? = null, )
LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array<String>? = null, whereClause: String? = null, whereArgs: Array<String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, )
LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )
- 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, )
- LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, )
- 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, )
+ LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, jwt: String? = null, )
+ 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 _identityVerificationService: IdentityVerificationService, )
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, )
+ 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, private val _identityVerificationService: IdentityVerificationService, )
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, )
- LongParameterList:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, )
+ LongParameterList:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, )
LongParameterList:UpdateSubscriptionOperation.kt$UpdateSubscriptionOperation$(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus)
+ LongParameterList:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, )
LongParameterList:UserSwitcher.kt$UserSwitcher$( private val preferencesService: IPreferencesService, private val operationRepo: IOperationRepo, private val services: ServiceProvider, private val idManager: IDManager = IDManager, private val identityModelStore: IdentityModelStore, private val propertiesModelStore: PropertiesModelStore, private val subscriptionModelStore: SubscriptionModelStore, private val configModel: ConfigModel, private val oneSignalUtils: OneSignalUtils = OneSignalUtils, private val carrierName: String? = null, private val deviceOS: String? = null, private val androidUtils: AndroidUtils = AndroidUtils, private val appContextProvider: () -> Context, )
LoopWithTooManyJumpStatements:ModelStore.kt$ModelStore$for (index in jsonArray.length() - 1 downTo 0) { val newModel = create(jsonArray.getJSONObject(index)) ?: continue /* * NOTE: Migration fix for bug introduced in 5.1.12 * The following check is intended for the operation model store. * When the call to this method moved out of the operation model store's initializer, * duplicate operations could be cached. * See https://github.com/OneSignal/OneSignal-Android-SDK/pull/2099 */ val hasExisting = models.any { it.id == newModel.id } if (hasExisting) { Logging.debug("ModelStore<$name>: load - operation.id: ${newModel.id} already exists in the store.") continue } models.add(0, newModel) // listen for changes to this model newModel.subscribe(this) }
MagicNumber:ApplicationService.kt$ApplicationService$50
@@ -266,6 +279,7 @@
MagicNumber:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$4
MagicNumber:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$99
MagicNumber:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$404
+ MatchingDeclarationName:ExecutorsIvExtensions.kt$IvBackendParams
MemberNameEqualsClassName:OneSignal.kt$OneSignal$private val oneSignal: IOneSignal by lazy { OneSignalImp() }
NestedBlockDepth:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
NestedBlockDepth:InfluenceManager.kt$InfluenceManager$private fun attemptSessionUpgrade( entryAction: AppEntryAction, directId: String? = null, )
@@ -287,8 +301,9 @@
RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e
ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution
ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean
- ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean
ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model?
+ ReturnCount:ExecutorsIvExtensions.kt$internal fun resolveIvBackendParams( op: Operation, onesignalId: String, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): IvBackendParams
+ ReturnCount:ExecutorsIvExtensions.kt$internal fun resolveIvJwt( op: Operation, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): String?
ReturnCount:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome
ReturnCount:HttpClient.kt$HttpClient$private suspend fun makeRequest( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse
ReturnCount:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
@@ -301,10 +316,11 @@
ReturnCount:Model.kt$Model$protected fun getOptIntProperty( name: String, create: (() -> Int?)? = null, ): Int?
ReturnCount:Model.kt$Model$protected fun getOptLongProperty( name: String, create: (() -> Long?)? = null, ): Long?
ReturnCount:Model.kt$Model$protected inline fun <reified T : Enum<T>> getOptEnumProperty(name: String): T?
+ ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean
ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation?
+ ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean
ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.handleFailUnauthorized( startingOp: OperationRepo.OperationQueueItem, ops: List<OperationRepo.OperationQueueItem>, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): Boolean
ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.hasValidJwtIfRequired( jwtTokenStore: JwtTokenStore, op: com.onesignal.core.internal.operations.Operation, ivBehaviorActive: Boolean, ): Boolean
- ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean
ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List<Influence>, ): OutcomeEvent?
ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendUniqueOutcomeEvent( name: String, sessionInfluences: List<Influence>, ): OutcomeEvent?
ReturnCount:PermissionsViewModel.kt$PermissionsViewModel$private fun shouldShowSettings( permission: String, shouldShowRationaleAfter: Boolean, ): Boolean
@@ -329,11 +345,13 @@
SwallowedException:JSONUtils.kt$JSONUtils$t: Throwable
SwallowedException:PermissionsActivity.kt$PermissionsActivity$e: ClassNotFoundException
SwallowedException:PreferencesService.kt$PreferencesService$ex: Exception
+ SwallowedException:PreferencesService.kt$PreferencesService$t: Throwable
SwallowedException:SyncJobService.kt$SyncJobService$e: Exception
SwallowedException:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$t: Throwable
ThrowsCount:OneSignalImp.kt$OneSignalImp$private suspend fun waitUntilInitInternal(operationName: String? = null)
TooGenericExceptionCaught:AndroidUtils.kt$AndroidUtils$e: Throwable
TooGenericExceptionCaught:DeviceUtils.kt$DeviceUtils$t: Throwable
+ TooGenericExceptionCaught:FeatureFlagsRefreshService.kt$FeatureFlagsRefreshService$e: Exception
TooGenericExceptionCaught:HttpClient.kt$HttpClient$e: Throwable
TooGenericExceptionCaught:HttpClient.kt$HttpClient$t: Throwable
TooGenericExceptionCaught:JSONUtils.kt$JSONUtils$t: Throwable
@@ -343,6 +361,7 @@
TooGenericExceptionCaught:PreferenceStoreFix.kt$PreferenceStoreFix$e: Throwable
TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$e: Throwable
TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$ex: Exception
+ TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$t: Throwable
TooGenericExceptionCaught:SyncJobService.kt$SyncJobService$e: Exception
TooGenericExceptionCaught:ThreadUtils.kt$e: Exception
TooGenericExceptionCaught:TrackGooglePurchase.kt$TrackGooglePurchase$e: Throwable
@@ -421,10 +440,6 @@
UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResponse
UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResult
UndocumentedPublicClass:IOutcomeEvent.kt$IOutcomeEvent
- UndocumentedPublicClass:IParamsBackendService.kt$FCMParamsObject
- UndocumentedPublicClass:IParamsBackendService.kt$IParamsBackendService
- UndocumentedPublicClass:IParamsBackendService.kt$InfluenceParamsObject
- UndocumentedPublicClass:IParamsBackendService.kt$ParamsObject
UndocumentedPublicClass:IPreferencesService.kt$PreferenceOneSignalKeys
UndocumentedPublicClass:IPreferencesService.kt$PreferencePlayerPurchasesKeys
UndocumentedPublicClass:IPreferencesService.kt$PreferenceStores
@@ -557,8 +572,6 @@
UndocumentedPublicFunction:Logging.kt$Logging$@JvmStatic fun warn( message: String, throwable: Throwable? = null, )
UndocumentedPublicFunction:Logging.kt$Logging$fun addListener(listener: ILogListener)
UndocumentedPublicFunction:Logging.kt$Logging$fun removeListener(listener: ILogListener)
- UndocumentedPublicFunction:LoginHelper.kt$LoginHelper$suspend fun login( externalId: String, jwtBearerToken: String? = null, )
- UndocumentedPublicFunction:LogoutHelper.kt$LogoutHelper$fun logout()
UndocumentedPublicFunction:Model.kt$Model$fun <T> setListProperty( name: String, value: List<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
UndocumentedPublicFunction:Model.kt$Model$fun <T> setMapModelProperty( name: String, value: MapModel<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
UndocumentedPublicFunction:Model.kt$Model$fun <T> setOptListProperty( name: String, value: List<T>?, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
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 d1ea2036c2..2b6e1267f4 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
@@ -159,17 +159,10 @@ internal class HttpClient(
con.doOutput = true
}
- logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties)
-
- if (jsonBody != null) {
- val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody)
- val sendBytes = strJsonBody.toByteArray(charset("UTF-8"))
- con.setFixedLengthStreamingMode(sendBytes.size)
- val outputStream = con.outputStream
- outputStream.write(sendBytes)
- }
-
- // H E A D E R S
+ // H E A D E R S — must be set before any body write below. `getOutputStream()`
+ // (and `setFixedLengthStreamingMode`) commit the request line + headers to the
+ // wire; `setRequestProperty` after that point either throws IllegalStateException
+ // or is silently dropped, depending on the HttpURLConnection implementation.
if (headers?.cacheKey != null) {
val eTag =
@@ -195,6 +188,20 @@ internal class HttpClient(
con.setRequestProperty("OneSignal-Session-Duration", headers.sessionDuration.toString())
}
+ if (headers?.jwt != null) {
+ con.setRequestProperty("Authorization", "Bearer ${headers.jwt}")
+ }
+
+ logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties)
+
+ if (jsonBody != null) {
+ val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody)
+ val sendBytes = strJsonBody.toByteArray(charset("UTF-8"))
+ con.setFixedLengthStreamingMode(sendBytes.size)
+ val outputStream = con.outputStream
+ outputStream.write(sendBytes)
+ }
+
// Network request is made from getResponseCode()
httpResponse = con.responseCode
diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt
index f566fd04fc..8a0f3e7c95 100644
--- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt
@@ -17,4 +17,9 @@ data class OptionalHeaders(
* Used to track delay between session start and request
*/
val sessionDuration: Long? = null,
+ /**
+ * JWT bearer token for identity verification. When non-null, sent as
+ * `Authorization: Bearer ` on the request.
+ */
+ val jwt: String? = null,
)
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 4278d8002b..b59dd71917 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,
)
}
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 7bcf23fdb2..45089fcb13 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
@@ -22,6 +22,7 @@ interface ISubscriptionBackendService {
aliasLabel: String,
aliasValue: String,
subscription: SubscriptionObject,
+ jwt: String? = null,
): Pair?
/**
@@ -35,6 +36,7 @@ interface ISubscriptionBackendService {
appId: String,
subscriptionId: String,
subscription: SubscriptionObject,
+ jwt: String? = null,
): RywData?
/**
@@ -46,6 +48,7 @@ interface ISubscriptionBackendService {
suspend fun deleteSubscription(
appId: String,
subscriptionId: String,
+ jwt: String? = null,
)
/**
@@ -61,11 +64,15 @@ interface ISubscriptionBackendService {
subscriptionId: String,
aliasLabel: String,
aliasValue: String,
+ jwt: String? = null,
)
/**
* Given an existing subscription, retrieve all identities associated to it.
*
+ * Note: this endpoint is not used when `jwt_required == true`; the v4→v5 migration path it
+ * supports is explicitly blocked under IV. See [LoginUserFromSubscriptionOperationExecutor].
+ *
* @param appId The ID of the OneSignal application this subscription exists under.
* @param subscriptionId The ID of the subscription to retrieve identities for.
*
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 4cec114b5a..b849fc4c42 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
@@ -24,6 +24,7 @@ interface IUserBackendService {
identities: Map,
subscriptions: List,
properties: Map,
+ jwt: String? = null,
): CreateUserResponse
// TODO: Change to send only the push subscription, optimally
@@ -48,6 +49,7 @@ interface IUserBackendService {
properties: PropertiesObject,
refreshDeviceMetadata: Boolean,
propertyiesDelta: PropertiesDeltasObject,
+ jwt: String? = null,
): RywData?
/**
@@ -65,6 +67,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..1bfd8c14af 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
@@ -4,6 +4,7 @@ import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.putMap
import com.onesignal.common.toMap
import com.onesignal.core.internal.http.IHttpClient
+import com.onesignal.core.internal.http.impl.OptionalHeaders
import com.onesignal.user.internal.backend.IIdentityBackendService
import org.json.JSONObject
@@ -15,12 +16,14 @@ 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, OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -36,8 +39,10 @@ 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", OptionalHeaders(jwt = 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 a2266d4d36..995e45393e 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
@@ -7,6 +7,7 @@ import com.onesignal.common.safeLong
import com.onesignal.common.safeString
import com.onesignal.common.toMap
import com.onesignal.core.internal.http.IHttpClient
+import com.onesignal.core.internal.http.impl.OptionalHeaders
import com.onesignal.user.internal.backend.ISubscriptionBackendService
import com.onesignal.user.internal.backend.SubscriptionObject
import org.json.JSONObject
@@ -19,11 +20,13 @@ internal class SubscriptionBackendService(
aliasLabel: String,
aliasValue: String,
subscription: SubscriptionObject,
+ jwt: String?,
): Pair? {
val jsonSubscription = JSONConverter.convertToJSON(subscription)
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, OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -50,12 +53,13 @@ internal class SubscriptionBackendService(
appId: String,
subscriptionId: String,
subscription: SubscriptionObject,
+ jwt: String?,
): RywData? {
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, OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -76,8 +80,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", OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -89,12 +94,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, OptionalHeaders(jwt = 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 1a1514018f..464c4963da 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
@@ -6,6 +6,7 @@ import com.onesignal.common.putMap
import com.onesignal.common.safeLong
import com.onesignal.common.safeString
import com.onesignal.core.internal.http.IHttpClient
+import com.onesignal.core.internal.http.impl.OptionalHeaders
import com.onesignal.user.internal.backend.CreateUserResponse
import com.onesignal.user.internal.backend.IUserBackendService
import com.onesignal.user.internal.backend.PropertiesDeltasObject
@@ -21,6 +22,7 @@ internal class UserBackendService(
identities: Map,
subscriptions: List,
properties: Map,
+ jwt: String?,
): CreateUserResponse {
val requestJSON = JSONObject()
@@ -39,7 +41,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, OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -55,6 +57,7 @@ internal class UserBackendService(
properties: PropertiesObject,
refreshDeviceMetadata: Boolean,
propertyiesDelta: PropertiesDeltasObject,
+ jwt: String?,
): RywData? {
val jsonObject =
JSONObject()
@@ -68,7 +71,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, OptionalHeaders(jwt = jwt))
if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
@@ -90,8 +93,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", OptionalHeaders(jwt = 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/customEvents/ICustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt
index 92474635ab..8c624f1f76 100644
--- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt
@@ -20,5 +20,6 @@ interface ICustomEventBackendService {
eventName: String,
eventProperties: String?,
metadata: CustomEventMetadata,
+ jwt: String? = null,
): ExecutionResponse
}
diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt
index 096fa67456..b6ff7e7033 100644
--- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt
@@ -3,6 +3,7 @@ package com.onesignal.user.internal.customEvents.impl
import com.onesignal.common.DateUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.core.internal.http.IHttpClient
+import com.onesignal.core.internal.http.impl.OptionalHeaders
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.user.internal.customEvents.ICustomEventBackendService
@@ -21,6 +22,7 @@ internal class CustomEventBackendService(
eventName: String,
eventProperties: String?,
metadata: CustomEventMetadata,
+ jwt: String?,
): ExecutionResponse {
val body = JSONObject()
body.put("name", eventName)
@@ -42,7 +44,7 @@ internal class CustomEventBackendService(
body.put("payload", payload)
val jsonObject = JSONObject().put("events", JSONArray().put(body))
- val response = httpClient.post("apps/$appId/custom_events", jsonObject)
+ val response = httpClient.post("apps/$appId/custom_events", jsonObject, OptionalHeaders(jwt = 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/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt
index 2e1046e6c6..03e1265c50 100644
--- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt
@@ -6,6 +6,7 @@ import com.onesignal.common.NetworkUtils
import com.onesignal.common.OneSignalUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.core.internal.application.IApplicationService
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.device.IDeviceService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
@@ -13,12 +14,15 @@ import com.onesignal.core.internal.operations.IOperationExecutor
import com.onesignal.core.internal.operations.Operation
import com.onesignal.user.internal.customEvents.ICustomEventBackendService
import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.TrackCustomEventOperation
internal class CustomEventOperationExecutor(
private val customEventBackendService: ICustomEventBackendService,
private val applicationService: IApplicationService,
private val deviceService: IDeviceService,
+ private val jwtTokenStore: JwtTokenStore,
+ private val identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(CUSTOM_EVENT)
@@ -40,6 +44,7 @@ internal class CustomEventOperationExecutor(
try {
when (operation) {
is TrackCustomEventOperation -> {
+ val jwt = resolveJwt(operation, jwtTokenStore, identityVerificationService)
customEventBackendService.sendCustomEvent(
operation.appId,
operation.onesignalId,
@@ -48,6 +53,7 @@ internal class CustomEventOperationExecutor(
operation.eventName,
operation.eventProperties,
eventMetadataJson,
+ jwt,
)
}
}
@@ -57,6 +63,8 @@ internal class CustomEventOperationExecutor(
return when (responseType) {
NetworkUtils.ResponseStatusType.RETRYABLE ->
ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds)
+ NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
+ ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds)
else ->
ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}
diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt
new file mode 100644
index 0000000000..4eb85e02ce
--- /dev/null
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt
@@ -0,0 +1,117 @@
+package com.onesignal.user.internal.operations.impl.executors
+
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
+import com.onesignal.core.internal.operations.Operation
+import com.onesignal.debug.internal.logging.Logging
+import com.onesignal.user.internal.backend.IdentityConstants
+import com.onesignal.user.internal.jwt.JwtTokenStore
+
+/**
+ * IV-specific parameter resolution for operation executors. Base-class call sites dispatch via
+ * `if (newCodePathsRun) resolveIvBackendParams(...) else IvBackendParams.legacyFor(...)`;
+ * the inner `ivBehaviorActive` check keeps Phase 3 users (new code path on, IV behavior off)
+ * on legacy alias/jwt values so they exercise the dispatch without any behavioral change.
+ *
+ * `ivBehaviorActive` is passed in by the executor (read from the injected
+ * `IdentityVerificationService`) — extension functions are not classes and cannot inject.
+ */
+
+/** Alias + JWT triplet passed into backend calls. */
+internal data class IvBackendParams(
+ val aliasLabel: String,
+ val aliasValue: String,
+ val jwt: String?,
+) {
+ companion object {
+ /** Values used when the new code path is off or IV behavior is inactive. Byte-for-byte identical to pre-IV behavior. */
+ fun legacyFor(onesignalId: String) = IvBackendParams(IdentityConstants.ONESIGNAL_ID, onesignalId, null)
+ }
+}
+
+/**
+ * Combined outer + inner gate for executors that just want "the right alias/JWT for this op."
+ * Replaces the call-site `if (newCodePathsRun) resolveIvBackendParams(...) else legacyFor(...)`
+ * boilerplate. Phase 1 (newCodePathsRun=false) skips the new code path entirely and returns
+ * legacy values directly, preserving the rollout-safety property that Phase 1 traffic does not
+ * exercise the IV resolution code.
+ */
+internal fun resolveBackendParams(
+ op: Operation,
+ onesignalId: String,
+ jwtTokenStore: JwtTokenStore,
+ identityVerificationService: IdentityVerificationService,
+): IvBackendParams =
+ if (identityVerificationService.newCodePathsRun) {
+ resolveIvBackendParams(op, onesignalId, jwtTokenStore, identityVerificationService.ivBehaviorActive)
+ } else {
+ IvBackendParams.legacyFor(onesignalId)
+ }
+
+/**
+ * Resolves alias + JWT for a backend call. Intended to be called only when
+ * `newCodePathsRun` is true; base-class dispatch handles the outer gate. Most executors should
+ * call [resolveBackendParams] which encapsulates the outer gate.
+ *
+ * - IV behavior inactive (Phase 3) → legacy values; no alias switch, no JWT attach.
+ * - IV behavior active + op has externalId → `external_id` alias + JWT from [JwtTokenStore].
+ * Note: `hasValidJwtIfRequired` already keeps anon ops and missing-JWT ops out of dispatch while
+ * IV is active, so the null-externalId / missing-JWT branches below are defensive only.
+ *
+ * [onesignalId] is passed explicitly because the base [Operation] class doesn't expose it;
+ * concrete executors all have their own onesignalId field on the operation they're about to send.
+ */
+internal fun resolveIvBackendParams(
+ op: Operation,
+ onesignalId: String,
+ jwtTokenStore: JwtTokenStore,
+ ivBehaviorActive: Boolean,
+): IvBackendParams {
+ if (!ivBehaviorActive) return IvBackendParams.legacyFor(onesignalId)
+ val externalId = op.externalId
+ if (externalId == null) {
+ Logging.error("IV active but op has null externalId; falling back to onesignal_id")
+ return IvBackendParams.legacyFor(onesignalId)
+ }
+ return IvBackendParams(IdentityConstants.EXTERNAL_ID, externalId, jwtTokenStore.getJwt(externalId))
+}
+
+/**
+ * Combined outer + inner gate for executors that need only a JWT (no alias switch). Replaces
+ * the call-site `if (newCodePathsRun) resolveIvJwt(...) else null` boilerplate. Phase 1
+ * (newCodePathsRun=false) skips the new code path entirely and returns null directly,
+ * preserving the rollout-safety property that Phase 1 traffic does not exercise IV resolution.
+ */
+internal fun resolveJwt(
+ op: Operation,
+ jwtTokenStore: JwtTokenStore,
+ identityVerificationService: IdentityVerificationService,
+): String? =
+ if (identityVerificationService.newCodePathsRun) {
+ resolveIvJwt(op, jwtTokenStore, identityVerificationService.ivBehaviorActive)
+ } else {
+ null
+ }
+
+/**
+ * Resolves a JWT-only parameter (used by endpoints that don't take alias label/value, e.g.
+ * subscription update/delete, custom events). Same inner gating as [resolveIvBackendParams].
+ * Intended to be called only when `newCodePathsRun` is true; most executors should call
+ * [resolveJwt] which encapsulates the outer gate.
+ */
+internal fun resolveIvJwt(
+ op: Operation,
+ jwtTokenStore: JwtTokenStore,
+ ivBehaviorActive: Boolean,
+): String? {
+ if (!ivBehaviorActive) return null
+ val externalId = op.externalId ?: return null
+ return jwtTokenStore.getJwt(externalId)
+}
+
+/**
+ * LoginUserFromSubscription path is blocked under IV — the backend endpoint the v4→v5 upgrade path
+ * relies on is not allowed when `jwt_required == true`. Returns `true` when the executor should
+ * short-circuit to `FAIL_NORETRY`. Called only when `newCodePathsRun` is true; Phase 3 users
+ * (behavior inactive) fall through and use the legacy migration path.
+ */
+internal fun shouldFailLoginUserFromSubscription(ivBehaviorActive: Boolean): Boolean = ivBehaviorActive
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..424690bb36 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
@@ -3,15 +3,16 @@ package com.onesignal.user.internal.operations.impl.executors
import com.onesignal.common.NetworkUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.IOperationExecutor
import com.onesignal.core.internal.operations.Operation
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.backend.IIdentityBackendService
-import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.builduser.IRebuildUserService
import com.onesignal.user.internal.identity.IdentityModelStore
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.DeleteAliasOperation
import com.onesignal.user.internal.operations.SetAliasOperation
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
@@ -21,6 +22,8 @@ internal class IdentityOperationExecutor(
private val _identityModelStore: IdentityModelStore,
private val _buildUserService: IRebuildUserService,
private val _newRecordState: NewRecordsState,
+ private val _jwtTokenStore: JwtTokenStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(SET_ALIAS, DELETE_ALIAS)
@@ -44,12 +47,14 @@ internal class IdentityOperationExecutor(
val lastOperation = operations.last()
if (lastOperation is SetAliasOperation) {
+ val params = resolveBackendParams(lastOperation, lastOperation.onesignalId, _jwtTokenStore, _identityVerificationService)
try {
_identityBackend.setAlias(
lastOperation.appId,
- IdentityConstants.ONESIGNAL_ID,
- lastOperation.onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
mapOf(lastOperation.label to lastOperation.value),
+ params.jwt,
)
// ensure the now created alias is in the model as long as the user is still current.
@@ -87,12 +92,14 @@ internal class IdentityOperationExecutor(
}
}
} else if (lastOperation is DeleteAliasOperation) {
+ val params = resolveBackendParams(lastOperation, lastOperation.onesignalId, _jwtTokenStore, _identityVerificationService)
try {
_identityBackend.deleteAlias(
lastOperation.appId,
- IdentityConstants.ONESIGNAL_ID,
- lastOperation.onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
lastOperation.label,
+ params.jwt,
)
// ensure the now deleted alias is not in the model as long as the user is still current.
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 2894f52631..76d5551854 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
@@ -3,6 +3,7 @@ package com.onesignal.user.internal.operations.impl.executors
import com.onesignal.common.NetworkUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.IOperationExecutor
@@ -20,6 +21,7 @@ internal class LoginUserFromSubscriptionOperationExecutor(
private val _subscriptionBackend: ISubscriptionBackendService,
private val _identityModelStore: IdentityModelStore,
private val _propertiesModelStore: PropertiesModelStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(LOGIN_USER_FROM_SUBSCRIPTION_USER)
@@ -27,6 +29,13 @@ internal class LoginUserFromSubscriptionOperationExecutor(
override suspend fun execute(operations: List): ExecutionResponse {
Logging.debug("LoginUserFromSubscriptionOperationExecutor(operation: $operations)")
+ // The getIdentityFromSubscription endpoint isn't allowed when `jwt_required == true`; the
+ // v4→v5 migration path this executor exists for only applies to non-IV apps. Drop on IV.
+ if (_identityVerificationService.newCodePathsRun && shouldFailLoginUserFromSubscription(_identityVerificationService.ivBehaviorActive)) {
+ Logging.warn("LoginUserFromSubscriptionOperation is not supported when identity verification is enabled. Dropping.")
+ return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
+ }
+
if (operations.size > 1) {
throw Exception("Only supports one operation! Attempted operations:\n$operations")
}
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 1d68b9a26b..4b980e801a 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
@@ -12,6 +12,7 @@ import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.config.ConfigModelStore
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.device.IDeviceService
import com.onesignal.core.internal.language.ILanguageContext
import com.onesignal.core.internal.operations.ExecutionResponse
@@ -24,6 +25,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.identity.IdentityModelStore
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.CreateSubscriptionOperation
import com.onesignal.user.internal.operations.DeleteSubscriptionOperation
import com.onesignal.user.internal.operations.LoginUserOperation
@@ -47,6 +49,8 @@ internal class LoginUserOperationExecutor(
private val _subscriptionsModelStore: SubscriptionModelStore,
private val _configModelStore: ConfigModelStore,
private val _languageContext: ILanguageContext,
+ private val _jwtTokenStore: JwtTokenStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(LOGIN_USER)
@@ -74,10 +78,18 @@ internal class LoginUserOperationExecutor(
if (!containsSubscriptionOperation && loginUserOp.externalId == null) {
return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}
- if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null) {
+ if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null ||
+ _identityVerificationService.ivBehaviorActive
+ ) {
// When there is no existing user to attempt to associate with the externalId provided, we go right to
// createUser. If there is no externalId provided this is an insert, if there is this will be an
// "upsert with retrieval" as the user may already exist.
+ //
+ // Under IV, also skip the optimistic SetAliasOperation: that inline op identifies the
+ // target user by `onesignal_id = existingOnesignalId`, but IV's alias-resolution would
+ // rewrite the call to identify by the new (not-yet-registered) `external_id`, producing
+ // a 404 or idempotent success against the wrong user. createUser's identities-map path
+ // handles the merge correctly through backend upsert semantics.
return createUser(loginUserOp, operations)
} else {
// before we create a user we attempt to associate the user defined by existingOnesignalId with the
@@ -169,7 +181,9 @@ internal class LoginUserOperationExecutor(
try {
val subscriptionList = subscriptions.toList()
- val response = _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties)
+ val jwt = resolveJwt(createUserOperation, _jwtTokenStore, _identityVerificationService)
+ val response =
+ _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties, jwt)
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.
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 d7bfa0f671..3d3784b0db 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
@@ -5,6 +5,7 @@ import com.onesignal.common.TimeUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.core.internal.config.ConfigModelStore
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.IOperationExecutor
@@ -12,11 +13,11 @@ import com.onesignal.core.internal.operations.Operation
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.backend.IUserBackendService
-import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.backend.SubscriptionObjectType
import com.onesignal.user.internal.builduser.IRebuildUserService
import com.onesignal.user.internal.identity.IdentityModel
import com.onesignal.user.internal.identity.IdentityModelStore
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.RefreshUserOperation
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
import com.onesignal.user.internal.properties.PropertiesModel
@@ -34,6 +35,8 @@ internal class RefreshUserOperationExecutor(
private val _configModelStore: ConfigModelStore,
private val _buildUserService: IRebuildUserService,
private val _newRecordState: NewRecordsState,
+ private val _jwtTokenStore: JwtTokenStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(REFRESH_USER)
@@ -54,12 +57,14 @@ internal class RefreshUserOperationExecutor(
}
private suspend fun getUser(op: RefreshUserOperation): ExecutionResponse {
+ val params = resolveBackendParams(op, op.onesignalId, _jwtTokenStore, _identityVerificationService)
try {
val response =
_userBackend.getUser(
op.appId,
- IdentityConstants.ONESIGNAL_ID,
- op.onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
+ params.jwt,
)
if (op.onesignalId != _identityModelStore.model.onesignalId) {
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 e6c82f1a70..3d240636a5 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
@@ -14,6 +14,7 @@ import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.config.ConfigModelStore
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.device.IDeviceService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
@@ -22,10 +23,10 @@ import com.onesignal.core.internal.operations.Operation
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.backend.ISubscriptionBackendService
-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.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.CreateSubscriptionOperation
import com.onesignal.user.internal.operations.DeleteSubscriptionOperation
import com.onesignal.user.internal.operations.TransferSubscriptionOperation
@@ -44,6 +45,8 @@ internal class SubscriptionOperationExecutor(
private val _buildUserService: IRebuildUserService,
private val _newRecordState: NewRecordsState,
private val _consistencyManager: IConsistencyManager,
+ private val _jwtTokenStore: JwtTokenStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(CREATE_SUBSCRIPTION, UPDATE_SUBSCRIPTION, DELETE_SUBSCRIPTION, TRANSFER_SUBSCRIPTION)
@@ -107,12 +110,14 @@ internal class SubscriptionOperationExecutor(
AndroidUtils.getAppVersion(_applicationService.appContext),
)
+ val params = resolveBackendParams(createOperation, createOperation.onesignalId, _jwtTokenStore, _identityVerificationService)
val result =
_subscriptionBackend.createSubscription(
createOperation.appId,
- IdentityConstants.ONESIGNAL_ID,
- createOperation.onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
subscription,
+ params.jwt,
) ?: return ExecutionResponse(ExecutionResult.SUCCESS)
val backendSubscriptionId = result.first
@@ -190,7 +195,8 @@ internal class SubscriptionOperationExecutor(
AndroidUtils.getAppVersion(_applicationService.appContext),
)
- val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription)
+ val jwt = resolveJwt(lastOperation, _jwtTokenStore, _identityVerificationService)
+ val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription, jwt)
if (rywData != null) {
_consistencyManager.setRywData(startingOperation.onesignalId, IamFetchRywTokenKey.SUBSCRIPTION, rywData)
@@ -203,6 +209,8 @@ internal class SubscriptionOperationExecutor(
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.MISSING -> {
if (ex.statusCode == 404 &&
listOf(
@@ -240,12 +248,14 @@ internal class SubscriptionOperationExecutor(
// TODO: whenever the end-user changes users, we need to add the read-your-write token here, currently no code to handle the re-fetch IAMs
private suspend fun transferSubscription(startingOperation: TransferSubscriptionOperation): ExecutionResponse {
+ val params = resolveBackendParams(startingOperation, startingOperation.onesignalId, _jwtTokenStore, _identityVerificationService)
try {
_subscriptionBackend.transferSubscription(
startingOperation.appId,
startingOperation.subscriptionId,
- IdentityConstants.ONESIGNAL_ID,
- startingOperation.onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
+ params.jwt,
)
} catch (ex: BackendException) {
val responseType = NetworkUtils.getResponseStatusType(ex.statusCode)
@@ -253,6 +263,8 @@ internal class SubscriptionOperationExecutor(
return when (responseType) {
NetworkUtils.ResponseStatusType.RETRYABLE ->
ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds)
+ NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
+ ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds)
else ->
ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}
@@ -276,8 +288,9 @@ internal class SubscriptionOperationExecutor(
}
private suspend fun deleteSubscription(op: DeleteSubscriptionOperation): ExecutionResponse {
+ val jwt = resolveJwt(op, _jwtTokenStore, _identityVerificationService)
try {
- _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId)
+ _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId, jwt)
// remove the subscription model as a HYDRATE in case for some reason it still exists.
_subscriptionModelStore.remove(op.subscriptionId, ModelChangeTags.HYDRATE)
@@ -300,6 +313,8 @@ internal class SubscriptionOperationExecutor(
}
NetworkUtils.ResponseStatusType.RETRYABLE ->
ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds)
+ NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
+ ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds)
else ->
ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}
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 e529035ec1..f611c60ea5 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
@@ -6,6 +6,7 @@ import com.onesignal.common.consistency.enums.IamFetchRywTokenKey
import com.onesignal.common.consistency.models.IConsistencyManager
import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.IOperationExecutor
@@ -13,12 +14,12 @@ import com.onesignal.core.internal.operations.Operation
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.backend.IUserBackendService
-import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.backend.PropertiesDeltasObject
import com.onesignal.user.internal.backend.PropertiesObject
import com.onesignal.user.internal.backend.PurchaseObject
import com.onesignal.user.internal.builduser.IRebuildUserService
import com.onesignal.user.internal.identity.IdentityModelStore
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.DeleteTagOperation
import com.onesignal.user.internal.operations.SetPropertyOperation
import com.onesignal.user.internal.operations.SetTagOperation
@@ -35,6 +36,8 @@ internal class UpdateUserOperationExecutor(
private val _buildUserService: IRebuildUserService,
private val _newRecordState: NewRecordsState,
private val _consistencyManager: IConsistencyManager,
+ private val _jwtTokenStore: JwtTokenStore,
+ private val _identityVerificationService: IdentityVerificationService,
) : IOperationExecutor {
override val operations: List
get() = listOf(SET_TAG, DELETE_TAG, SET_PROPERTY, TRACK_SESSION_START, TRACK_SESSION_END, TRACK_PURCHASE)
@@ -137,15 +140,20 @@ internal class UpdateUserOperationExecutor(
}
if (appId != null && onesignalId != null) {
+ // Grouped update ops all belong to the same user (queue grouping is per-user), so
+ // pulling externalId from `operations.first()` is safe and represents the batch.
+ val firstOp = operations.first()
+ val params = resolveBackendParams(firstOp, onesignalId, _jwtTokenStore, _identityVerificationService)
try {
val rywData =
_userBackend.updateUser(
appId,
- IdentityConstants.ONESIGNAL_ID,
- onesignalId,
+ params.aliasLabel,
+ params.aliasValue,
propertiesObject,
refreshDeviceMetadata,
deltasObject,
+ params.jwt,
)
if (rywData != null) {
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt
index 0fc991b861..93f9776b31 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt
@@ -255,4 +255,48 @@ class HttpClientTests : FunSpec({
response2 shouldBe null
response3 shouldNotBe null
}
+
+ test("Authorization header is set when OptionalHeaders.jwt is provided") {
+ // Given
+ val mocks = Mocks()
+
+ // When
+ mocks.httpClient.get("URL", OptionalHeaders(jwt = "abc.def.ghi"))
+ mocks.httpClient.post("URL", JSONObject(), OptionalHeaders(jwt = "abc.def.ghi"))
+
+ // Then
+ for (connection in mocks.factory.connections) {
+ connection.getRequestProperty("Authorization") shouldBe "Bearer abc.def.ghi"
+ }
+ }
+
+ test("Authorization header is NOT set when OptionalHeaders.jwt is null") {
+ // Given
+ val mocks = Mocks()
+
+ // When
+ mocks.httpClient.get("URL", OptionalHeaders(jwt = null))
+ mocks.httpClient.post("URL", JSONObject())
+
+ // Then
+ for (connection in mocks.factory.connections) {
+ connection.getRequestProperty("Authorization") shouldBe null
+ }
+ }
+
+ test("POST + JWT does not throw (headers must be set before body write)") {
+ // Given: MockHttpURLConnection now throws IllegalStateException if setRequestProperty is
+ // called after connect()/getOutputStream()/getResponseCode (matching real Android behavior).
+ // This test fails if the HttpClient regresses to setting Authorization after the body write.
+ val mocks = Mocks()
+ mocks.response.status = 200
+ mocks.response.responseBody = "{}"
+
+ // When
+ val response = mocks.httpClient.post("URL", JSONObject().put("k", "v"), OptionalHeaders(jwt = "the-jwt"))
+
+ // Then: the request completed without the mock throwing, and the header actually stuck.
+ response.throwable shouldBe null
+ mocks.factory.connections.last().getRequestProperty("Authorization") shouldBe "Bearer the-jwt"
+ }
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt
index 595f6fcd51..45381b774c 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt
@@ -32,6 +32,13 @@ internal class MockHttpConnectionFactory(
url: URL?,
private val mockResponse: MockResponse,
) : HttpURLConnection(url) {
+ // Real HttpURLConnection (both stock JDK and Android's OkHttp-backed impl) commits the
+ // request line + headers to the wire when `getOutputStream()` / `setFixedLengthStreamingMode`
+ // is used to stream a body, and rejects `setRequestProperty` afterward. The stock mock
+ // lets headers be set at any time, which hides header-ordering bugs. Simulate the real
+ // contract so HttpClientTests will fail fast if headers are set after the body begins.
+ private var headersCommitted: Boolean = false
+
override fun disconnect() {}
override fun usingProxy(): Boolean {
@@ -40,6 +47,14 @@ internal class MockHttpConnectionFactory(
@Throws(IOException::class)
override fun connect() {
+ headersCommitted = true
+ }
+
+ override fun setRequestProperty(key: String, value: String?) {
+ if (headersCommitted) {
+ throw IllegalStateException("Cannot set request property '$key' after connection is made")
+ }
+ super.setRequestProperty(key, value)
}
override fun getHeaderField(name: String): String? {
@@ -48,6 +63,7 @@ internal class MockHttpConnectionFactory(
@Throws(IOException::class)
override fun getResponseCode(): Int {
+ headersCommitted = true
if (mockResponse.mockRequestTime != null) {
try {
Thread.sleep(mockResponse.mockRequestTime!!)
@@ -60,6 +76,7 @@ internal class MockHttpConnectionFactory(
}
override fun getOutputStream(): OutputStream {
+ headersCommitted = true
return NullOutputStream()
}
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt
index 4f9c377348..ba4e376473 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt
@@ -22,14 +22,30 @@ infix fun > T.shouldHaveEachItemEndWith(expected: Array()
+ fun register(listener: ILogListener): ILogListener {
+ registered.add(listener)
+ Logging.addListener(listener)
+ return listener
+ }
+
beforeAny {
Logging.logLevel = LogLevel.NONE
}
+ afterEach {
+ registered.forEach { Logging.removeListener(it) }
+ registered.clear()
+ }
+
test("addListener") {
// Given
val listener = TestLogLister()
- Logging.addListener(listener)
+ register(listener)
// When
Logging.debug("test")
@@ -41,8 +57,8 @@ class LoggingTests : FunSpec({
test("addListener twice") {
// Given
val listener = TestLogLister()
- Logging.addListener(listener)
- Logging.addListener(listener)
+ register(listener)
+ register(listener)
// When
Logging.debug("test")
@@ -54,7 +70,7 @@ class LoggingTests : FunSpec({
test("removeListener") {
// Given
val listener = TestLogLister()
- Logging.addListener(listener)
+ register(listener)
Logging.removeListener(listener)
// When
@@ -67,7 +83,7 @@ class LoggingTests : FunSpec({
test("removeListener twice") {
// Given
val listener = TestLogLister()
- Logging.addListener(listener)
+ register(listener)
Logging.removeListener(listener)
Logging.removeListener(listener)
@@ -81,7 +97,8 @@ class LoggingTests : FunSpec({
test("addListener nested") {
// Given
val nestedListener = TestLogLister()
- Logging.addListener { Logging.addListener(nestedListener) }
+ val outerListener = ILogListener { register(nestedListener) }
+ register(outerListener)
// When
Logging.debug("test")
@@ -101,7 +118,7 @@ class LoggingTests : FunSpec({
// Remove self from listeners
Logging.removeListener(listener)
}
- Logging.addListener(listener)
+ register(listener)
// When
Logging.debug("test")
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt
index 924d7f9f3f..3885c73d5d 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt
@@ -35,7 +35,7 @@ class CustomEventBackendServiceTests : FunSpec({
test("track event") {
// Given
val spyHttpClient = mockk()
- coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "")
+ coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "")
val customEventBackendService = CustomEventBackendService(spyHttpClient)
// When
@@ -80,6 +80,7 @@ class CustomEventBackendServiceTests : FunSpec({
payload.getJSONObject("os_sdk").toString() shouldBeEqual metadata.toJSONObject().toString()
payload.getString("proKey1") shouldBeEqual "proVal1"
},
+ any(),
)
}
}
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt
index d66ee47ebf..36c99e652f 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt
@@ -21,7 +21,7 @@ class IdentityBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(200, "{ identity: { aliasKey1: \"aliasValue1\"} }")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(200, "{ identity: { aliasKey1: \"aliasValue1\"} }")
val identityBackendService = IdentityBackendService(spyHttpClient)
val identities = mapOf("aliasKey1" to "aliasValue1")
@@ -38,6 +38,7 @@ class IdentityBackendServiceTests : FunSpec({
it.getJSONObject("identity").has("aliasKey1") shouldBe true
it.getJSONObject("identity").getString("aliasKey1") shouldBe "aliasValue1"
},
+ any(),
)
}
}
@@ -48,7 +49,7 @@ class IdentityBackendServiceTests : FunSpec({
val aliasValue = "11111111-1111-1111-1111-111111111111"
val aliasToDelete = "aliasKey1"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.delete(any()) } returns HttpResponse(200, "")
+ coEvery { spyHttpClient.delete(any(), any()) } returns HttpResponse(200, "")
val identityBackendService = IdentityBackendService(spyHttpClient)
// When
@@ -56,7 +57,7 @@ class IdentityBackendServiceTests : FunSpec({
// Then
coVerify {
- spyHttpClient.delete("apps/appId/users/by/$aliasLabel/$aliasValue/identity/$aliasToDelete")
+ spyHttpClient.delete("apps/appId/users/by/$aliasLabel/$aliasValue/identity/$aliasToDelete", any())
}
}
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt
index 0ff7a15f8f..5877ec5df9 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt
@@ -27,7 +27,7 @@ class SubscriptionBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "{ \"subscription\": { id: \"subscriptionId\" }, \"ryw_token\": \"123\"}")
+ coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "{ \"subscription\": { id: \"subscriptionId\" }, \"ryw_token\": \"123\"}")
val subscriptionBackendService = SubscriptionBackendService(spyHttpClient)
// When
@@ -55,6 +55,7 @@ class SubscriptionBackendServiceTests : FunSpec({
sub.getBoolean("enabled") shouldBe true
sub.getInt("notification_types") shouldBe 1
},
+ any(),
)
}
}
@@ -64,7 +65,7 @@ class SubscriptionBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(404, "NOT FOUND")
+ coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(404, "NOT FOUND")
val subscriptionBackendService = SubscriptionBackendService(spyHttpClient)
// When
@@ -100,6 +101,7 @@ class SubscriptionBackendServiceTests : FunSpec({
sub.getBoolean("enabled") shouldBe true
sub.getInt("notification_types") shouldBe 1
},
+ any(),
)
}
}
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt
index 0fa15fdcae..110c7566cb 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt
@@ -22,7 +22,7 @@ class UserBackendServiceTests : FunSpec({
test("create user with nothing throws an exception") {
// Given
val spyHttpClient = mockk()
- coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(403, "FORBIDDEN")
+ coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(403, "FORBIDDEN")
val userBackendService = UserBackendService(spyHttpClient)
val identities = mapOf()
val properties = mapOf()
@@ -44,7 +44,7 @@ class UserBackendServiceTests : FunSpec({
val osId = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
coEvery {
- spyHttpClient.post(any(), any())
+ spyHttpClient.post(any(), any(), any())
} returns HttpResponse(202, "{identity:{onesignal_id: \"$osId\", aliasLabel1: \"aliasValue1\"}, properties:{timezone_id: \"testTimeZone\", language: \"testLanguage\"}}")
val userBackendService = UserBackendService(spyHttpClient)
val identities = mapOf("aliasLabel1" to "aliasValue1")
@@ -70,6 +70,7 @@ class UserBackendServiceTests : FunSpec({
it.has("properties") shouldBe true
it.has("subscriptions") shouldBe false
},
+ any(),
)
}
}
@@ -79,7 +80,7 @@ class UserBackendServiceTests : FunSpec({
val osId = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
coEvery {
- spyHttpClient.post(any(), any())
+ spyHttpClient.post(any(), any(), any())
} returns HttpResponse(202, "{identity:{onesignal_id: \"$osId\"}, subscriptions:[{id:\"subscriptionId1\", type:\"AndroidPush\"}], properties:{timezone_id: \"testTimeZone\", language: \"testLanguage\"}}")
val userBackendService = UserBackendService(spyHttpClient)
val identities = mapOf()
@@ -109,6 +110,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONArray("subscriptions").getJSONObject(0).has("type") shouldBe true
it.getJSONArray("subscriptions").getJSONObject(0).getString("type") shouldBe "AndroidPush"
},
+ any(),
)
}
}
@@ -118,7 +120,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1"))
val propertiesDelta = PropertiesDeltasObject()
@@ -136,6 +138,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("properties").getJSONObject("tags").has("tagkey1") shouldBe true
it.getJSONObject("properties").getJSONObject("tags").getString("tagkey1") shouldBe "tagValue1"
},
+ any(),
)
}
}
@@ -145,7 +148,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(language = "newLanguage")
val propertiesDelta = PropertiesDeltasObject()
@@ -162,6 +165,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("properties").has("language") shouldBe true
it.getJSONObject("properties").getString("language") shouldBe "newLanguage"
},
+ any(),
)
}
}
@@ -171,7 +175,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { timezone_id: \"America/New_York\"}}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { timezone_id: \"America/New_York\"}}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(timezoneId = "America/New_York")
val propertiesDelta = PropertiesDeltasObject()
@@ -188,6 +192,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("properties").has("timezone_id") shouldBe true
it.getJSONObject("properties").getString("timezone_id") shouldBe "America/New_York"
},
+ any(),
)
}
}
@@ -197,7 +202,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { country: \"TV\"}}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { country: \"TV\"}}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(country = "TV")
val propertiesDelta = PropertiesDeltasObject()
@@ -214,6 +219,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("properties").has("country") shouldBe true
it.getJSONObject("properties").getString("country") shouldBe "TV"
},
+ any(),
)
}
}
@@ -223,7 +229,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { lat: 12.34, long: 45.67}}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { lat: 12.34, long: 45.67}}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(latitude = 12.34, longitude = 45.67)
val propertiesDelta = PropertiesDeltasObject()
@@ -242,6 +248,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("properties").has("long") shouldBe true
it.getJSONObject("properties").getDouble("long") shouldBe 45.67
},
+ any(),
)
}
}
@@ -251,7 +258,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: {} }")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: {} }")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1"))
val propertiesDelta = PropertiesDeltasObject()
@@ -266,6 +273,7 @@ class UserBackendServiceTests : FunSpec({
withArg {
it.getBoolean("refresh_device_metadata") shouldBe true
},
+ any(),
)
}
}
@@ -275,7 +283,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: {} }")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: {} }")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1"))
val propertiesDelta = PropertiesDeltasObject()
@@ -290,6 +298,7 @@ class UserBackendServiceTests : FunSpec({
withArg {
it.getBoolean("refresh_device_metadata") shouldBe false
},
+ any(),
)
}
}
@@ -299,7 +308,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { }}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { }}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject()
val propertiesDelta = PropertiesDeltasObject(sessionTime = 1111, sessionCount = 1)
@@ -318,6 +327,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("deltas").has("session_count") shouldBe true
it.getJSONObject("deltas").getInt("session_count") shouldBe 1
},
+ any(),
)
}
}
@@ -327,7 +337,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { }}")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { }}")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject()
val propertiesDelta =
@@ -366,6 +376,7 @@ class UserBackendServiceTests : FunSpec({
it.getJSONObject("deltas").getJSONArray("purchases").getJSONObject(1).has("amount") shouldBe true
it.getJSONObject("deltas").getJSONArray("purchases").getJSONObject(1).getDouble("amount") shouldBe 4444
},
+ any(),
)
}
}
@@ -375,7 +386,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(404, "NOT FOUND")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(404, "NOT FOUND")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1"))
val propertiesDelta = PropertiesDeltasObject()
@@ -396,7 +407,7 @@ class UserBackendServiceTests : FunSpec({
val aliasLabel = "onesignal_id"
val aliasValue = "11111111-1111-1111-1111-111111111111"
val spyHttpClient = mockk()
- coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(403, "FORBIDDEN")
+ coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(403, "FORBIDDEN")
val userBackendService = UserBackendService(spyHttpClient)
val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1"))
val propertiesDelta = PropertiesDeltasObject()
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt
index 044d4c3726..5681a12a77 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt
@@ -3,12 +3,15 @@ package com.onesignal.user.internal.operations
import android.content.Context
import android.os.Build
import com.onesignal.common.OneSignalUtils
+import com.onesignal.common.exceptions.BackendException
import com.onesignal.core.internal.device.IDeviceService
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.customEvents.ICustomEventBackendService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
@@ -23,7 +26,7 @@ class CustomEventOperationExecutorTests : FunSpec({
test("execution of track event operation") {
// Given
val mockCustomEventBackendService = mockk()
- coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS)
+ coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS)
val mockApplicationService = MockHelper.applicationService()
val mockContext = mockk(relaxed = true)
@@ -36,7 +39,7 @@ class CustomEventOperationExecutorTests : FunSpec({
val properties = JSONObject().put("key", "value").toString()
val customEventOperationExecutor =
- CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService)
+ CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService, getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(TrackCustomEventOperation("appId", "onesignalId", null, 1, "event-name", properties))
// When
@@ -60,7 +63,40 @@ class CustomEventOperationExecutorTests : FunSpec({
it.deviceModel shouldBe deviceMode
it.deviceOS shouldBe deviceOS
},
+ null,
)
}
}
+
+ test("track event returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") {
+ // Given
+ val mockCustomEventBackendService = mockk()
+ coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any(), any()) } throws
+ BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5)
+
+ val mockApplicationService = MockHelper.applicationService()
+ every { mockApplicationService.appContext } returns mockk(relaxed = true)
+ val mockDeviceService = MockHelper.deviceService()
+ every { mockDeviceService.deviceType } returns IDeviceService.DeviceType.Android
+
+ val customEventOperationExecutor =
+ CustomEventOperationExecutor(
+ mockCustomEventBackendService,
+ mockApplicationService,
+ mockDeviceService,
+ getJwtTokenStore(),
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+ val operations =
+ listOf(
+ TrackCustomEventOperation("appId", "onesignalId", "ext-1", 1, "event-name", JSONObject().toString()),
+ )
+
+ // When
+ val response = customEventOperationExecutor.execute(operations)
+
+ // Then — must be FAIL_UNAUTHORIZED so OperationRepo.handleFailUnauthorized fires.
+ response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED
+ response.retryAfterSeconds shouldBe 5
+ }
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt
index 9890563333..aaee22323e 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt
@@ -1,12 +1,31 @@
package com.onesignal.user.internal.operations
import com.onesignal.core.internal.config.ConfigModelStore
+import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.time.impl.Time
import com.onesignal.mocks.MockHelper
+import com.onesignal.mocks.MockPreferencesService
+import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
+import io.mockk.every
+import io.mockk.mockk
class ExecutorMocks {
companion object {
fun getNewRecordState(configModelStore: ConfigModelStore = MockHelper.configModelStore()) = NewRecordsState(Time(), configModelStore)
+
+ /** Real (empty) JWT store backed by a mock preferences service. `getJwt` returns null for all keys. */
+ internal fun getJwtTokenStore() = JwtTokenStore(MockPreferencesService())
+
+ /** Mocked [IdentityVerificationService] with both gates returning false by default — IV inactive, new code paths off. */
+ internal fun getIdentityVerificationService(
+ newCodePathsRun: Boolean = false,
+ ivBehaviorActive: Boolean = false,
+ ): IdentityVerificationService {
+ val mock = mockk(relaxed = true)
+ every { mock.newCodePathsRun } returns newCodePathsRun
+ every { mock.ivBehaviorActive } returns ivBehaviorActive
+ return mock
+ }
}
}
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt
new file mode 100644
index 0000000000..a8de55e5dd
--- /dev/null
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt
@@ -0,0 +1,100 @@
+package com.onesignal.user.internal.operations
+
+import com.onesignal.debug.LogLevel
+import com.onesignal.debug.internal.logging.Logging
+import com.onesignal.mocks.MockPreferencesService
+import com.onesignal.user.internal.backend.IdentityConstants
+import com.onesignal.user.internal.jwt.JwtTokenStore
+import com.onesignal.user.internal.operations.impl.executors.IvBackendParams
+import com.onesignal.user.internal.operations.impl.executors.resolveIvBackendParams
+import com.onesignal.user.internal.operations.impl.executors.resolveIvJwt
+import com.onesignal.user.internal.operations.impl.executors.shouldFailLoginUserFromSubscription
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class ExecutorsIvExtensionsTests : FunSpec({
+ beforeAny {
+ Logging.logLevel = LogLevel.NONE
+ }
+
+ test("resolveIvBackendParams returns legacy values when ivBehaviorActive is false (Phase 3)") {
+ // Given: new code path on but behavior inactive
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ jwtStore.putJwt("ext-1", "jwt-value")
+ val op = LoginUserOperation("app", "os-1", "ext-1", null)
+
+ // When
+ val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = false)
+
+ // Then: onesignal_id alias, no JWT
+ params shouldBe IvBackendParams(IdentityConstants.ONESIGNAL_ID, "os-1", null)
+ }
+
+ test("resolveIvBackendParams returns external_id alias + JWT when IV active and externalId present") {
+ // Given
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ jwtStore.putJwt("ext-1", "jwt-value")
+ val op = LoginUserOperation("app", "os-1", "ext-1", null)
+
+ // When
+ val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true)
+
+ // Then
+ params shouldBe IvBackendParams(IdentityConstants.EXTERNAL_ID, "ext-1", "jwt-value")
+ }
+
+ test("resolveIvBackendParams falls back to onesignal_id when IV active but externalId null (defensive)") {
+ // Given: IV active, op has no externalId (shouldn't happen per hasValidJwtIfRequired gating)
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ val op = LoginUserOperation("app", "os-1", null, null)
+
+ // When
+ val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true)
+
+ // Then
+ params shouldBe IvBackendParams(IdentityConstants.ONESIGNAL_ID, "os-1", null)
+ }
+
+ test("resolveIvBackendParams returns null JWT when IV active but no JWT stored (defensive)") {
+ // Given: IV active, externalId set, but no JWT in store
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ val op = LoginUserOperation("app", "os-1", "ext-1", null)
+
+ // When
+ val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true)
+
+ // Then: alias still switches to external_id but jwt is null
+ params shouldBe IvBackendParams(IdentityConstants.EXTERNAL_ID, "ext-1", null)
+ }
+
+ test("resolveIvJwt returns null when ivBehaviorActive is false") {
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ jwtStore.putJwt("ext-1", "jwt-value")
+ val op = LoginUserOperation("app", "os-1", "ext-1", null)
+
+ resolveIvJwt(op, jwtStore, ivBehaviorActive = false) shouldBe null
+ }
+
+ test("resolveIvJwt returns stored JWT when IV active and externalId present") {
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ jwtStore.putJwt("ext-1", "jwt-value")
+ val op = LoginUserOperation("app", "os-1", "ext-1", null)
+
+ resolveIvJwt(op, jwtStore, ivBehaviorActive = true) shouldBe "jwt-value"
+ }
+
+ test("resolveIvJwt returns null when IV active but op is anonymous") {
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ val op = LoginUserOperation("app", "os-1", null, null)
+
+ resolveIvJwt(op, jwtStore, ivBehaviorActive = true) shouldBe null
+ }
+
+ test("shouldFailLoginUserFromSubscription returns false when ivBehaviorActive is false") {
+ shouldFailLoginUserFromSubscription(ivBehaviorActive = false) shouldBe false
+ }
+
+ test("shouldFailLoginUserFromSubscription returns true when ivBehaviorActive is true") {
+ shouldFailLoginUserFromSubscription(ivBehaviorActive = true) shouldBe true
+ }
+})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt
index 057ef2c96a..3e3cc45a2e 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt
@@ -5,11 +5,15 @@ import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.Operation
import com.onesignal.mocks.MockHelper
+import com.onesignal.mocks.MockPreferencesService
import com.onesignal.user.internal.backend.IIdentityBackendService
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.builduser.IRebuildUserService
import com.onesignal.user.internal.identity.IdentityModel
import com.onesignal.user.internal.identity.IdentityModelStore
+import com.onesignal.user.internal.jwt.JwtTokenStore
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
import io.kotest.core.spec.style.FunSpec
@@ -39,7 +43,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1"))
// When
@@ -69,7 +73,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1"))
// When
@@ -90,7 +94,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1"))
// When
@@ -111,7 +115,7 @@ class IdentityOperationExecutorTests : FunSpec({
every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } returns null
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1"))
// When
@@ -134,7 +138,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 }
val newRecordState = getNewRecordState(mockConfigModelStore).also { it.add("onesignalId") }
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState)
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1"))
// When
@@ -160,7 +164,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1"))
// When
@@ -183,7 +187,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1"))
// When
@@ -203,7 +207,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1"))
// When
@@ -225,7 +229,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockBuildUserService = mockk()
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState())
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1"))
// When
@@ -250,7 +254,7 @@ class IdentityOperationExecutorTests : FunSpec({
val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 }
val newRecordState = getNewRecordState(mockConfigModelStore).also { it.add("onesignalId") }
val identityOperationExecutor =
- IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState)
+ IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1"))
// When
@@ -259,4 +263,85 @@ class IdentityOperationExecutorTests : FunSpec({
// Then
response.result shouldBe ExecutionResult.FAIL_RETRY
}
+
+ test("set alias uses external_id alias and attaches JWT when IV active") {
+ // Given
+ val mockIdentityBackendService = mockk()
+ coEvery { mockIdentityBackendService.setAlias(any(), any(), any(), any(), any()) } returns mapOf()
+
+ val mockIdentityModel = mockk()
+ every { mockIdentityModel.onesignalId } returns "onesignalId"
+ every { mockIdentityModel.setStringProperty(any(), any(), any()) } just runs
+
+ val mockIdentityModelStore = mockk()
+ every { mockIdentityModelStore.model } returns mockIdentityModel
+
+ val mockBuildUserService = mockk()
+
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ jwtStore.putJwt("ext-1", "the-jwt")
+
+ val identityOperationExecutor =
+ IdentityOperationExecutor(
+ mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), jwtStore,
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+ val operations = listOf(SetAliasOperation("appId", "onesignalId", "ext-1", "aliasKey1", "aliasValue1"))
+
+ // When
+ val response = identityOperationExecutor.execute(operations)
+
+ // Then
+ response.result shouldBe ExecutionResult.SUCCESS
+ coVerify(exactly = 1) {
+ mockIdentityBackendService.setAlias(
+ "appId",
+ IdentityConstants.EXTERNAL_ID,
+ "ext-1",
+ mapOf("aliasKey1" to "aliasValue1"),
+ "the-jwt",
+ )
+ }
+ }
+
+ test("set alias keeps onesignal_id alias and null JWT in Phase 3 (newCodePathsRun + ivBehaviorActive=false)") {
+ // Given: new code path on but IV behavior off — extension runs but must return legacy values
+ val mockIdentityBackendService = mockk()
+ coEvery { mockIdentityBackendService.setAlias(any(), any(), any(), any(), any()) } returns mapOf()
+
+ val mockIdentityModel = mockk()
+ every { mockIdentityModel.onesignalId } returns "onesignalId"
+ every { mockIdentityModel.setStringProperty(any(), any(), any()) } just runs
+
+ val mockIdentityModelStore = mockk()
+ every { mockIdentityModelStore.model } returns mockIdentityModel
+
+ val mockBuildUserService = mockk()
+
+ val jwtStore = JwtTokenStore(MockPreferencesService())
+ // A JWT is stored; Phase 3 must NOT attach it.
+ jwtStore.putJwt("ext-1", "the-jwt")
+
+ val identityOperationExecutor =
+ IdentityOperationExecutor(
+ mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), jwtStore,
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = false),
+ )
+ val operations = listOf(SetAliasOperation("appId", "onesignalId", "ext-1", "aliasKey1", "aliasValue1"))
+
+ // When
+ val response = identityOperationExecutor.execute(operations)
+
+ // Then: onesignal_id alias, null JWT
+ response.result shouldBe ExecutionResult.SUCCESS
+ coVerify(exactly = 1) {
+ mockIdentityBackendService.setAlias(
+ "appId",
+ IdentityConstants.ONESIGNAL_ID,
+ "onesignalId",
+ mapOf("aliasKey1" to "aliasValue1"),
+ null,
+ )
+ }
+ }
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt
index e3c8c1cfb4..0085360235 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt
@@ -14,6 +14,8 @@ import com.onesignal.user.internal.backend.PropertiesObject
import com.onesignal.user.internal.backend.SubscriptionObject
import com.onesignal.user.internal.backend.SubscriptionObjectType
import com.onesignal.user.internal.identity.IdentityModel
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
import com.onesignal.user.internal.properties.PropertiesModel
@@ -77,6 +79,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -121,6 +124,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -149,7 +153,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations =
listOf(
LoginUserOperation(appId, localOneSignalId, null, null),
@@ -177,7 +181,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null))
// When
@@ -215,6 +219,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null))
@@ -243,7 +248,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
// When
@@ -279,7 +284,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
// When
@@ -315,7 +320,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
// When
@@ -353,7 +358,7 @@ class LoginUserOperationExecutorTests : FunSpec({
val mockSubscriptionsModelStore = mockk()
val loginUserOperationExecutor =
- LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
+ LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
// When
@@ -404,6 +409,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -508,6 +514,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -596,6 +603,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -670,6 +678,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -735,6 +744,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
// anonymous Login request
val operations = listOf(LoginUserOperation(appId, localOneSignalId, null, null))
@@ -781,6 +791,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
MockHelper.configModelStore(),
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
// send PUSH then EMAIL (local IDs 1,2) — order differs from backend response
@@ -845,6 +856,7 @@ class LoginUserOperationExecutorTests : FunSpec({
mockSubscriptionsModelStore,
configModelStore,
MockHelper.languageContext(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val ops =
@@ -869,4 +881,65 @@ class LoginUserOperationExecutorTests : FunSpec({
configModelStore.model.pushSubscriptionId shouldBe remoteSubscriptionId1
coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) }
}
+
+ test("IV active: loginUser with existingOnesignalId + externalId goes straight to createUser, skipping optimistic SetAliasOperation") {
+ // Given: IV active. Under legacy or Phase 3 this input would hit the optimistic-merge
+ // SetAliasOperation path; under IV we must skip that because IdentityOperationExecutor
+ // would resolve alias to (external_id, newExternalId) and target the wrong user.
+ val mockUserBackendService = mockk()
+ coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns
+ CreateUserResponse(
+ mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId),
+ PropertiesObject(),
+ listOf(),
+ )
+
+ // Strict mock: if the optimistic-merge path is reached, the test fails because no
+ // `execute` stub is registered on IdentityOperationExecutor.
+ val mockIdentityOperationExecutor = mockk()
+
+ val mockIdentityModelStore = MockHelper.identityModelStore()
+ val mockPropertiesModelStore = MockHelper.propertiesModelStore()
+ val mockSubscriptionsModelStore = mockk()
+
+ val loginUserOperationExecutor =
+ LoginUserOperationExecutor(
+ mockIdentityOperationExecutor,
+ AndroidMockHelper.applicationService(),
+ MockHelper.deviceService(),
+ mockUserBackendService,
+ mockIdentityModelStore,
+ mockPropertiesModelStore,
+ mockSubscriptionsModelStore,
+ MockHelper.configModelStore(),
+ MockHelper.languageContext(),
+ getJwtTokenStore(),
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+
+ // LoginUserOperation has existingOnesignalId AND externalId — the input shape that
+ // triggers the optimistic-merge branch under legacy.
+ val operations =
+ listOf(
+ LoginUserOperation(appId, localOneSignalId, "new-external-id", "existing-osid"),
+ )
+
+ // When
+ val response = loginUserOperationExecutor.execute(operations)
+
+ // Then
+ response.result shouldBe ExecutionResult.SUCCESS
+ coVerify(exactly = 1) {
+ mockUserBackendService.createUser(
+ appId,
+ mapOf(IdentityConstants.EXTERNAL_ID to "new-external-id"),
+ any(),
+ any(),
+ any(),
+ )
+ }
+ // IdentityOperationExecutor must NOT be invoked (strict mock enforces this via kotest's
+ // unstubbed-call failure; coVerify(exactly = 0) makes the expectation explicit).
+ coVerify(exactly = 0) { mockIdentityOperationExecutor.execute(any()) }
+ }
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt
index 0f3765c557..43aa2ef827 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt
@@ -14,6 +14,8 @@ 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.IdentityModel
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState
import com.onesignal.user.internal.operations.impl.executors.RefreshUserOperationExecutor
import com.onesignal.user.internal.properties.PropertiesModel
@@ -107,6 +109,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
mockConfigModelStore,
mockBuildUserService,
getNewRecordState(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
@@ -191,6 +194,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
MockHelper.configModelStore(),
mockBuildUserService,
getNewRecordState(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
@@ -230,6 +234,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
MockHelper.configModelStore(),
mockBuildUserService,
getNewRecordState(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
@@ -265,6 +270,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
MockHelper.configModelStore(),
mockBuildUserService,
getNewRecordState(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
@@ -300,6 +306,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
MockHelper.configModelStore(),
mockBuildUserService,
getNewRecordState(),
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
@@ -337,6 +344,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
MockHelper.configModelStore(),
mockBuildUserService,
newRecordState,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null))
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 78f8ffc47f..19afa4c4dd 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
@@ -13,6 +13,8 @@ import com.onesignal.user.internal.backend.ISubscriptionBackendService
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.backend.SubscriptionObjectType
import com.onesignal.user.internal.builduser.IRebuildUserService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState
import com.onesignal.user.internal.operations.impl.executors.SubscriptionOperationExecutor
import com.onesignal.user.internal.subscriptions.SubscriptionModel
@@ -68,6 +70,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -129,6 +132,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -180,6 +184,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -236,6 +241,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -292,6 +298,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
newRecordState,
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -336,6 +343,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -383,6 +391,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -455,6 +464,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -518,6 +528,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -571,6 +582,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -626,6 +638,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
newRecordState,
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -669,6 +682,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -703,6 +717,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -738,6 +753,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -773,6 +789,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
newRecordState,
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -814,6 +831,7 @@ class SubscriptionOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
@@ -837,4 +855,100 @@ class SubscriptionOperationExecutorTests :
mockConsistencyManager.setRywData(remoteOneSignalId, IamFetchRywTokenKey.SUBSCRIPTION, rywData)
}
}
+
+ test("update subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") {
+ // Given
+ val mockSubscriptionBackendService = mockk()
+ coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any(), any()) } throws
+ BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5)
+
+ val subscriptionOperationExecutor =
+ SubscriptionOperationExecutor(
+ mockSubscriptionBackendService,
+ MockHelper.deviceService(),
+ AndroidMockHelper.applicationService(),
+ mockk(),
+ MockHelper.configModelStore(),
+ mockk(),
+ getNewRecordState(),
+ mockConsistencyManager,
+ getJwtTokenStore(),
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+ val operations =
+ listOf(
+ UpdateSubscriptionOperation(
+ appId, remoteOneSignalId, "ext-1", remoteSubscriptionId,
+ SubscriptionType.PUSH, true, "pushToken2", SubscriptionStatus.SUBSCRIBED,
+ ),
+ )
+
+ // When
+ val response = subscriptionOperationExecutor.execute(operations)
+
+ // Then — must be FAIL_UNAUTHORIZED so OperationRepo.handleFailUnauthorized fires.
+ response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED
+ response.retryAfterSeconds shouldBe 5
+ }
+
+ test("delete subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") {
+ // Given
+ val mockSubscriptionBackendService = mockk()
+ coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any(), any()) } throws
+ BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5)
+
+ val subscriptionOperationExecutor =
+ SubscriptionOperationExecutor(
+ mockSubscriptionBackendService,
+ MockHelper.deviceService(),
+ AndroidMockHelper.applicationService(),
+ mockk(),
+ MockHelper.configModelStore(),
+ mockk(),
+ getNewRecordState(),
+ mockConsistencyManager,
+ getJwtTokenStore(),
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+ val operations =
+ listOf(DeleteSubscriptionOperation(appId, remoteOneSignalId, "ext-1", remoteSubscriptionId))
+
+ // When
+ val response = subscriptionOperationExecutor.execute(operations)
+
+ // Then
+ response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED
+ response.retryAfterSeconds shouldBe 5
+ }
+
+ test("transfer subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") {
+ // Given
+ val mockSubscriptionBackendService = mockk()
+ coEvery {
+ mockSubscriptionBackendService.transferSubscription(any(), any(), any(), any(), any())
+ } throws BackendException(403, "FORBIDDEN", retryAfterSeconds = 5)
+
+ val subscriptionOperationExecutor =
+ SubscriptionOperationExecutor(
+ mockSubscriptionBackendService,
+ MockHelper.deviceService(),
+ AndroidMockHelper.applicationService(),
+ mockk(),
+ MockHelper.configModelStore(),
+ mockk(),
+ getNewRecordState(),
+ mockConsistencyManager,
+ getJwtTokenStore(),
+ getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
+ )
+ val operations =
+ listOf(TransferSubscriptionOperation(appId, remoteOneSignalId, "ext-1", remoteSubscriptionId))
+
+ // When
+ val response = subscriptionOperationExecutor.execute(operations)
+
+ // Then
+ response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED
+ response.retryAfterSeconds shouldBe 5
+ }
})
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt
index 2e0c122cbc..21e3bfdf17 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt
@@ -10,6 +10,8 @@ import com.onesignal.mocks.MockHelper
import com.onesignal.user.internal.backend.IUserBackendService
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.builduser.IRebuildUserService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService
+import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore
import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState
import com.onesignal.user.internal.operations.impl.executors.UpdateUserOperationExecutor
import com.onesignal.user.internal.properties.PropertiesModel
@@ -56,6 +58,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1"))
@@ -96,6 +99,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -158,6 +162,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -203,6 +208,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -269,6 +275,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =
listOf(
@@ -317,6 +324,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1"))
@@ -350,6 +358,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
newRecordState,
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1"))
@@ -380,6 +389,7 @@ class UpdateUserOperationExecutorTests :
mockBuildUserService,
getNewRecordState(),
mockConsistencyManager,
+ getJwtTokenStore(), getIdentityVerificationService(),
)
val operations =