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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<ID>ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _application: IApplicationService</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _configModelStore: ConfigModelStore</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _consistencyManager: IConsistencyManager</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _deviceService: IDeviceService</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityModelStore: IdentityModelStore</ID>
<ID>ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityOperationExecutor: IdentityOperationExecutor</ID>
Expand Down Expand Up @@ -155,7 +156,6 @@
<ID>ConstructorParameterNaming:UserBackendService.kt$UserBackendService$private val _httpClient: IHttpClient</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _customEventController: ICustomEventController</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _identityModelStore: IdentityModelStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _jwtTokenStore: JwtTokenStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _languageContext: ILanguageContext</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _propertiesModelStore: PropertiesModelStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _subscriptionManager: ISubscriptionManager</ID>
Expand All @@ -174,7 +174,6 @@
<ID>ForbiddenComment:HttpClient.kt$HttpClient$// TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT?</ID>
<ID>ForbiddenComment:IPreferencesService.kt$PreferenceOneSignalKeys$* (String) The serialized IAMs TODO: This isn't currently used, determine if actually needed for cold start IAM fetch delay</ID>
<ID>ForbiddenComment:IUserBackendService.kt$IUserBackendService$// TODO: Change to send only the push subscription, optimally</ID>
<ID>ForbiddenComment:LogoutHelper.kt$LogoutHelper$// TODO: remove JWT Token for all future requests.</ID>
<ID>ForbiddenComment:ParamsBackendService.kt$ParamsBackendService$// TODO: New</ID>
<ID>ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO after we remove IAM from being an activity window we may be able to remove this handler</ID>
<ID>ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO improve this method</ID>
Expand Down Expand Up @@ -213,7 +212,7 @@
<ID>LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array&lt;String>? = null, whereClause: String? = null, whereArgs: Array&lt;String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, )</ID>
<ID>LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )</ID>
<ID>LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, jwt: String? = null, )</ID>
<ID>LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, )</ID>
<ID>LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, private val _consistencyManager: IConsistencyManager, )</ID>
<ID>LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, )</ID>
<ID>LongParameterList:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, )</ID>
<ID>LongParameterList:SubscriptionObject.kt$SubscriptionObject$( val id: String? = null, val type: SubscriptionObjectType? = null, val token: String? = null, val enabled: Boolean? = null, val notificationTypes: Int? = null, val sdk: String? = null, val deviceModel: String? = null, val deviceOS: String? = null, val rooted: Boolean? = null, val netType: Int? = null, val carrier: String? = null, val appVersion: String? = null, )</ID>
Expand Down Expand Up @@ -311,6 +310,7 @@
<ID>ReturnCount:JSONUtils.kt$JSONUtils$fun compareJSONArrays( jsonArray1: JSONArray?, jsonArray2: JSONArray?, ): Boolean</ID>
<ID>ReturnCount:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private suspend fun loginUser(loginUserOp: LoginUserFromSubscriptionOperation): ExecutionResponse</ID>
<ID>ReturnCount:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun loginUser( loginUserOp: LoginUserOperation, operations: List&lt;Operation>, ): ExecutionResponse</ID>
<ID>ReturnCount:LogoutHelper.kt$LogoutHelper$internal fun switchUser(): LogoutEnqueueContext?</ID>
<ID>ReturnCount:Model.kt$Model$protected fun getOptBigDecimalProperty( name: String, create: (() -> BigDecimal?)? = null, ): BigDecimal?</ID>
<ID>ReturnCount:Model.kt$Model$protected fun getOptDoubleProperty( name: String, create: (() -> Double?)? = null, ): Double?</ID>
<ID>ReturnCount:Model.kt$Model$protected fun getOptFloatProperty( name: String, create: (() -> Float?)? = null, ): Float?</ID>
Expand All @@ -320,6 +320,7 @@
<ID>ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean</ID>
<ID>ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation?</ID>
<ID>ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean</ID>
<ID>ReturnCount:OperationRepo.kt$OperationRepo$private fun shouldSuppressAnonymousOp(op: Operation): Boolean</ID>
<ID>ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.handleFailUnauthorized( startingOp: OperationRepo.OperationQueueItem, ops: List&lt;OperationRepo.OperationQueueItem>, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): Boolean</ID>
<ID>ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.hasValidJwtIfRequired( jwtTokenStore: JwtTokenStore, op: com.onesignal.core.internal.operations.Operation, ivBehaviorActive: Boolean, ): Boolean</ID>
<ID>ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List&lt;Influence>, ): OutcomeEvent?</ID>
Expand Down Expand Up @@ -413,7 +414,7 @@
<ID>TooManyFunctions:OutcomeEventsController.kt$OutcomeEventsController : IOutcomeEventsControllerIStartableServiceISessionLifecycleHandler</ID>
<ID>TooManyFunctions:PreferencesService.kt$PreferencesService : IPreferencesServiceIStartableService</ID>
<ID>TooManyFunctions:SubscriptionManager.kt$SubscriptionManager : ISubscriptionManagerIModelStoreChangeHandlerISessionLifecycleHandler</ID>
<ID>TooManyFunctions:UserManager.kt$UserManager : IUserManagerISingletonModelStoreChangeHandlerIJwtUpdateListener</ID>
<ID>TooManyFunctions:UserManager.kt$UserManager : IUserManagerISingletonModelStoreChangeHandler</ID>
<ID>UndocumentedPublicClass:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$Callback</ID>
<ID>UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils</ID>
<ID>UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils$SchemaType</ID>
Expand Down Expand Up @@ -458,7 +459,6 @@
<ID>UndocumentedPublicClass:JSONConverter.kt$JSONConverter</ID>
<ID>UndocumentedPublicClass:JSONUtils.kt$JSONUtils</ID>
<ID>UndocumentedPublicClass:Logging.kt$Logging</ID>
<ID>UndocumentedPublicClass:LogoutHelper.kt$LogoutHelper</ID>
<ID>UndocumentedPublicClass:MigrationRecovery.kt$MigrationRecovery : IMigrationRecovery</ID>
<ID>UndocumentedPublicClass:NetworkUtils.kt$NetworkUtils</ID>
<ID>UndocumentedPublicClass:NetworkUtils.kt$NetworkUtils$ResponseStatusType</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.onesignal.user.internal.jwt.JwtRequirement
* Consumers (e.g. OperationRepo) wire post-HYDRATE behavior via [setOnJwtConfigHydratedHandler];
* the handler fires once per HYDRATE with `ivRequired = useIdentityVerification == REQUIRED`.
*/
internal class IdentityVerificationService(
class IdentityVerificationService(
private val featureManager: IFeatureManager,
private val configModelStore: ConfigModelStore,
) : IStartableService, ISingletonModelStoreChangeHandler<ConfigModel> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.onesignal.core.internal.features
/**
* Controls when remote config changes for a feature are applied.
*/
internal enum class FeatureActivationMode {
enum class FeatureActivationMode {
/**
* Apply config changes immediately during the current app run.
*/
Expand All @@ -20,7 +20,7 @@ internal enum class FeatureActivationMode {
*
* [key] values are **lowercase** strings as returned from remote config / Turbine `features` arrays.
*/
internal enum class FeatureFlag(
enum class FeatureFlag(
val key: String,
val activationMode: FeatureActivationMode
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.debug.internal.logging.Logging
import kotlinx.serialization.json.JsonObject

internal interface IFeatureManager {
interface IFeatureManager {
fun isEnabled(feature: FeatureFlag): Boolean

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ internal class OperationRepo(
operation: Operation,
flush: Boolean,
) {
if (shouldSuppressAnonymousOp(operation)) return

Logging.log(LogLevel.DEBUG, "OperationRepo.enqueue(operation: $operation, flush: $flush)")

operation.id = UUID.randomUUID().toString()
Expand All @@ -147,6 +149,8 @@ internal class OperationRepo(
operation: Operation,
flush: Boolean,
): Boolean {
if (shouldSuppressAnonymousOp(operation)) return false

Logging.log(LogLevel.DEBUG, "OperationRepo.enqueueAndWait(operation: $operation, force: $flush)")

operation.id = UUID.randomUUID().toString()
Expand All @@ -157,6 +161,26 @@ internal class OperationRepo(
return waiter.waitForWake()
}

/**
* Drop anonymous (externalId == null) operations at enqueue time when IV is required —
* they cannot be authenticated and would otherwise sit in the queue forever, blocked by
* `hasValidJwtIfRequired`. LoginUserOperation is exempt because it's enqueued
* intentionally during logout and purged later by [removeOperationsWithoutExternalId]
* if needed. Outer-gated on `newCodePathsRun` so Phase 1 customers stay byte-for-byte
* on the legacy enqueue path.
*/
private fun shouldSuppressAnonymousOp(op: Operation): Boolean {
if (!_identityVerificationService.newCodePathsRun) return false
if (op is LoginUserOperation) return false
val suppress =
_configModelStore.model.useIdentityVerification == JwtRequirement.REQUIRED &&
op.externalId == null
if (suppress) {
Logging.debug("OperationRepo: suppressing anonymous op under IV-required: $op")
}
return suppress
}

/**
* Only used inside this class, adds OperationQueueItem to queue
* WARNING: Never set flush=true until budget rules are added, even for internal use!
Expand Down Expand Up @@ -272,6 +296,18 @@ internal class OperationRepo(
val anonymous = queue.filter { it.operation.externalId == null }
anonymous.forEach { it.waiter?.wake(false) }
queue.removeAll(anonymous)
// IV=ON never transfers anonymous state; clear existingOnesignalId so the
// executor takes the createUser (upsert) path. The merge-anon-into-identified
// path can't dispatch under IV — anon user creation requires a JWT-less call
// the backend rejects — and a stale local-id existingOnesignalId would leave
// canStartExecute=false forever, deadlocking the queue.
queue.forEach { item ->
val op = item.operation
if (op is LoginUserOperation && op.existingOnesignalId != null) {
Logging.debug("OperationRepo: cleared existingOnesignalId on LoginUserOperation (was ${op.existingOnesignalId})")
op.existingOnesignalId = null
}
}
Logging.debug("OperationRepo: removeOperationsWithoutExternalId removed ${anonymous.size} of ${anonymous.size + queue.size} operations")
anonymous.map { it.operation.id }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.application.impl.ApplicationService
import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.features.FeatureFlag
import com.onesignal.core.internal.features.IFeatureManager
import com.onesignal.core.internal.operations.IOperationRepo
Expand Down Expand Up @@ -145,6 +146,7 @@ internal class OneSignalImp(
private val subscriptionModelStore: SubscriptionModelStore by lazy { services.getService<SubscriptionModelStore>() }
private val preferencesService: IPreferencesService by lazy { services.getService<IPreferencesService>() }
private val jwtTokenStore: JwtTokenStore by lazy { services.getService<JwtTokenStore>() }
private val identityVerificationService: IdentityVerificationService by lazy { services.getService<IdentityVerificationService>() }
private val listOfModules =
listOf(
"com.onesignal.notifications.NotificationsModule",
Expand Down Expand Up @@ -234,6 +236,8 @@ internal class OneSignalImp(
userSwitcher = userSwitcher,
operationRepo = operationRepo,
configModel = configModel,
subscriptionModelStore = subscriptionModelStore,
identityVerificationService = identityVerificationService,
lock = loginLogoutLock,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.operations.IOperationRepo
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.jwt.JwtRequirement
import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.operations.LoginUserOperation

Expand Down Expand Up @@ -56,8 +57,18 @@ internal class LoginHelper(
}

val newOneSignalId = identityModelStore.model.onesignalId
// Under IV-required, the merge-anon-into-identified path can't dispatch — the
// anon user was never created server-side (no JWT) so the local-id reference
// would deadlock LoginUserOperation.canStartExecute. Skip the link entirely so
// the executor takes the createUser (upsert) path.
val existingOneSignalId =
if (currentExternalId == null) currentOneSignalId else null
if (configModel.useIdentityVerification == JwtRequirement.REQUIRED) {
null
} else if (currentExternalId == null) {
currentOneSignalId
} else {
null
}

return LoginEnqueueContext(configModel.appId, newOneSignalId, externalId, existingOneSignalId)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.onesignal.user.internal

import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.config.impl.IdentityVerificationService
import com.onesignal.core.internal.operations.IOperationRepo
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.operations.LoginUserOperation
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore

class LogoutHelper(
internal class LogoutHelper(
private val identityModelStore: IdentityModelStore,
private val userSwitcher: UserSwitcher,
private val operationRepo: IOperationRepo,
private val configModel: ConfigModel,
private val subscriptionModelStore: SubscriptionModelStore,
private val identityVerificationService: IdentityVerificationService,
private val lock: Any,
) {
internal data class LogoutEnqueueContext(
Expand All @@ -29,11 +33,26 @@ class LogoutHelper(
return null
}

// Outer gate: dispatch to IV extension only on new code paths. The extension's
// inner gate (ivBehaviorActive) keeps Phase 3 users on the legacy logout flow.
val handled =
identityVerificationService.newCodePathsRun &&
switchUserIv(
userSwitcher,
subscriptionModelStore,
configModel,
identityVerificationService.ivBehaviorActive,
)
if (handled) {
// IV-required: subscription is internally disabled and the user-switch
// suppressed backend op enqueue. Don't enqueue anonymous LoginUserOperation —
// the anonymous user cannot authenticate without a JWT.
return null
}

// Create new device-scoped user (clears external ID)
userSwitcher.createAndSwitchToNewUser()

// TODO: remove JWT Token for all future requests.

return LogoutEnqueueContext(configModel.appId, identityModelStore.model.onesignalId)
}
}
Expand Down
Loading
Loading