diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml index 0530b2d0eb..ed239183e7 100644 --- a/OneSignalSDK/detekt/detekt-baseline-core.xml +++ b/OneSignalSDK/detekt/detekt-baseline-core.xml @@ -22,6 +22,7 @@ ConstructorParameterNaming:ConfigModelStoreListener.kt$ConfigModelStoreListener$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:ConfigModelStoreListener.kt$ConfigModelStoreListener$private val _paramsBackendService: IParamsBackendService ConstructorParameterNaming:ConfigModelStoreListener.kt$ConfigModelStoreListener$private val _subscriptionManager: ISubscriptionManager + ConstructorParameterNaming:CustomEventOperationExecutor.kt$CustomEventOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:DatabaseCursor.kt$DatabaseCursor$private val _cursor: Cursor ConstructorParameterNaming:DatabaseProvider.kt$DatabaseProvider$private val _application: IApplicationService ConstructorParameterNaming:DeviceService.kt$DeviceService$private val _applicationService: IApplicationService @@ -33,16 +34,26 @@ ConstructorParameterNaming:HttpConnectionFactory.kt$HttpConnectionFactory$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:IdentityBackendService.kt$IdentityBackendService$private val _httpClient: IHttpClient ConstructorParameterNaming:IdentityModelStoreListener.kt$IdentityModelStoreListener$private val _configModelStore: ConfigModelStore + ConstructorParameterNaming:IdentityModelStoreListener.kt$IdentityModelStoreListener$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _buildUserService: IRebuildUserService ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityBackend: IIdentityBackendService + ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _newRecordState: NewRecordsState + ConstructorParameterNaming:IdentityVerificationService.kt$IdentityVerificationService$private val _configModelStore: ConfigModelStore + ConstructorParameterNaming:IdentityVerificationService.kt$IdentityVerificationService$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:IdentityVerificationService.kt$IdentityVerificationService$private val _jwtTokenStore: JwtTokenStore + ConstructorParameterNaming:IdentityVerificationService.kt$IdentityVerificationService$private val _operationRepo: IOperationRepo + ConstructorParameterNaming:IdentityVerificationService.kt$IdentityVerificationService$private val _userManager: UserManager ConstructorParameterNaming:InfluenceDataRepository.kt$InfluenceDataRepository$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:InfluenceManager.kt$InfluenceManager$private val _applicationService: IApplicationService ConstructorParameterNaming:InfluenceManager.kt$InfluenceManager$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:InfluenceManager.kt$InfluenceManager$private val _sessionService: ISessionService ConstructorParameterNaming:InstallIdService.kt$InstallIdService$private val _prefs: IPreferencesService + ConstructorParameterNaming:JwtTokenStore.kt$JwtTokenStore$private val _prefs: IPreferencesService ConstructorParameterNaming:LanguageContext.kt$LanguageContext$private val _propertiesModelStore: PropertiesModelStore + ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService @@ -51,6 +62,7 @@ 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 _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 @@ -62,6 +74,8 @@ ConstructorParameterNaming:NewRecordsState.kt$NewRecordsState$private val _time: ITime ConstructorParameterNaming:OSDatabase.kt$OSDatabase$private val _outcomeTableProvider: OutcomeTableProvider ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _configModelStore: ConfigModelStore + ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _newRecordState: NewRecordsState ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _operationModelStore: OperationModelStore ConstructorParameterNaming:OperationRepo.kt$OperationRepo$private val _time: ITime @@ -81,6 +95,7 @@ ConstructorParameterNaming:PreferencesService.kt$PreferencesService$private val _applicationService: IApplicationService ConstructorParameterNaming:PreferencesService.kt$PreferencesService$private val _time: ITime ConstructorParameterNaming:PropertiesModelStoreListener.kt$PropertiesModelStoreListener$private val _configModelStore: ConfigModelStore + ConstructorParameterNaming:PropertiesModelStoreListener.kt$PropertiesModelStoreListener$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:RebuildUserService.kt$RebuildUserService$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:RebuildUserService.kt$RebuildUserService$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:RebuildUserService.kt$RebuildUserService$private val _propertiesModelStore: PropertiesModelStore @@ -93,6 +108,7 @@ 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 _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 @@ -123,6 +139,7 @@ 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 _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 @@ -132,8 +149,10 @@ ConstructorParameterNaming:TrackGooglePurchase.kt$TrackGooglePurchase$private val _operationRepo: IOperationRepo ConstructorParameterNaming:TrackGooglePurchase.kt$TrackGooglePurchase$private val _prefs: IPreferencesService ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _buildUserService: IRebuildUserService + ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _consistencyManager: IConsistencyManager ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _identityModelStore: IdentityModelStore + 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 @@ -192,15 +211,19 @@ LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun queryBoughtItems() 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:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, ) + 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, 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, ) 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, ) 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, ) + 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 _configModelStore: ConfigModelStore, private val _jwtTokenStore: JwtTokenStore, ) 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 @@ -229,6 +252,7 @@ MagicNumber:OSDatabase.kt$OSDatabase$8 MagicNumber:OSDatabase.kt$OSDatabase$9 MagicNumber:OneSignalDispatchers.kt$OneSignalDispatchers$1024 + MagicNumber:OneSignalImp.kt$OneSignalImp$8 MagicNumber:OperationRepo.kt$OperationRepo$1_000 MagicNumber:OutcomeEventsController.kt$OutcomeEventsController$1000 MagicNumber:PermissionsActivity.kt$PermissionsActivity$23 @@ -265,6 +289,7 @@ NestedBlockDepth:InfluenceManager.kt$InfluenceManager$private fun attemptSessionUpgrade( entryAction: AppEntryAction, directId: String? = null, ) NestedBlockDepth:JSONUtils.kt$JSONUtils$fun compareJSONArrays( jsonArray1: JSONArray?, jsonArray2: JSONArray?, ): Boolean NestedBlockDepth:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse + NestedBlockDepth:OperationRepo.kt$OperationRepo$internal suspend fun executeOperations(ops: List<OperationQueueItem>) NestedBlockDepth:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private suspend fun getUser(op: RefreshUserOperation): ExecutionResponse NestedBlockDepth:ServiceRegistration.kt$ServiceRegistrationReflection$override fun resolve(provider: IServiceProvider): Any? NestedBlockDepth:ServiceRegistration.kt$ServiceRegistrationReflection$private fun doesHaveAllParameters( constructor: Constructor<*>, provider: IServiceProvider, ): Boolean @@ -279,6 +304,7 @@ PrintStackTrace:TrackGooglePurchase.kt$TrackGooglePurchase$e PrintStackTrace:TrackGooglePurchase.kt$TrackGooglePurchase.<no name provided>$t RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e + ReturnCount:OperationRepo.kt$OperationRepo$private fun hasValidJwtIfRequired( identityVerificationEnabled: Boolean, op: Operation, ): Boolean 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 @@ -306,6 +332,7 @@ ReturnCount:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private suspend fun getUser(op: RefreshUserOperation): ExecutionResponse ReturnCount:ServiceRegistration.kt$ServiceRegistrationReflection$private fun doesHaveAllParameters( constructor: Constructor<*>, provider: IServiceProvider, ): Boolean ReturnCount:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private suspend fun createSubscription( createOperation: CreateSubscriptionOperation, operations: List<Operation>, ): ExecutionResponse + ReturnCount:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private suspend fun transferSubscription(startingOperation: TransferSubscriptionOperation): ExecutionResponse ReturnCount:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private suspend fun updateSubscription( startingOperation: UpdateSubscriptionOperation, operations: List<Operation>, ): ExecutionResponse ReturnCount:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse SpreadOperator:AndroidUtils.kt$AndroidUtils$(*packageInfo.requestedPermissions) @@ -609,6 +636,10 @@ UnusedPrivateMember:OperationRepo.kt$OperationRepo$private val _time: ITime UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'login'") UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'logout'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'login'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'logout'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'updateUserJwt'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before use") UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt index e20ddfc2ac..f42ed725c8 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt @@ -226,4 +226,46 @@ interface IOneSignal { * Logout the current user (suspend version). */ suspend fun logoutSuspend() + + /** + * Update the JWT bearer token for a user identified by [externalId]. Call this when + * a token is about to expire or after receiving an [IUserJwtInvalidatedListener] callback. + * + * @param externalId The external ID of the user whose token is being updated. + * @param token The new JWT bearer token. + */ + fun updateUserJwt( + externalId: String, + token: String, + ) + + /** + * Update the JWT bearer token for a user identified by [externalId] (suspend version). + * Call this when a token is about to expire or after receiving an [IUserJwtInvalidatedListener] + * callback. This suspend variant waits for the SDK to be initialized before proceeding. + * + * @param externalId The external ID of the user whose token is being updated. + * @param token The new JWT bearer token. + */ + suspend fun updateUserJwtSuspend( + externalId: String, + token: String, + ) + + /** + * Add a listener that will be called when a user's JWT is invalidated (e.g. expired + * or rejected by the server). Use this to provide a fresh token via [updateUserJwt]. + * + * The listener is invoked on a background thread; see [IUserJwtInvalidatedListener]. + * + * @param listener The listener to add. + */ + fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) + + /** + * Remove a previously added [IUserJwtInvalidatedListener]. + * + * @param listener The listener to remove. + */ + fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt new file mode 100644 index 0000000000..99b853ba49 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IUserJwtInvalidatedListener.kt @@ -0,0 +1,17 @@ +package com.onesignal + +/** + * Implement this interface and provide an instance to [OneSignal.addUserJwtInvalidatedListener] + * to be notified when the JWT for a user is invalidated. + * + * Callbacks are delivered on a background thread. + */ +interface IUserJwtInvalidatedListener { + /** + * Called when the JWT is invalidated for [UserJwtInvalidatedEvent.externalId]. + * Invoked on a background thread; see [IUserJwtInvalidatedListener] class documentation. + * + * @param event Describes which user's JWT was invalidated. + */ + fun onUserJwtInvalidated(event: UserJwtInvalidatedEvent) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt index 708bbe08f8..fbd07d0389 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt @@ -343,6 +343,42 @@ object OneSignal { @JvmStatic fun logout() = oneSignal.logout() + /** + * Update the JWT bearer token for a user identified by [externalId]. Call this when + * a token is about to expire or after receiving an [IUserJwtInvalidatedListener] callback. + * + * @param externalId The external ID of the user whose token is being updated. + * @param token The new JWT bearer token. + */ + @JvmStatic + fun updateUserJwt( + externalId: String, + token: String, + ) = oneSignal.updateUserJwt(externalId, token) + + /** + * Add a listener that will be called when a user's JWT is invalidated (e.g. expired + * or rejected by the server). Use this to provide a fresh token via [updateUserJwt]. + * + * The listener is invoked on a background thread; see [IUserJwtInvalidatedListener]. + * + * @param listener The listener to add. + */ + @JvmStatic + fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + oneSignal.addUserJwtInvalidatedListener(listener) + } + + /** + * Remove a previously added [IUserJwtInvalidatedListener]. + * + * @param listener The listener to remove. + */ + @JvmStatic + fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + oneSignal.removeUserJwtInvalidatedListener(listener) + } + private val oneSignal: IOneSignal by lazy { OneSignalImp() } @@ -405,6 +441,22 @@ object OneSignal { oneSignal.logoutSuspend() } + /** + * Update the JWT bearer token for a user identified by [externalId] (suspend version). + * Call this when a token is about to expire or after receiving an [IUserJwtInvalidatedListener] + * callback. + * + * @param externalId The external ID of the user whose token is being updated. + * @param token The new JWT bearer token. + */ + @JvmStatic + suspend fun updateUserJwtSuspend( + externalId: String, + token: String, + ) { + oneSignal.updateUserJwtSuspend(externalId, token) + } + /** * Used to retrieve services from the SDK when constructor dependency injection is not an * option. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt new file mode 100644 index 0000000000..291591ada8 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/UserJwtInvalidatedEvent.kt @@ -0,0 +1,9 @@ +package com.onesignal + +/** + * The event passed into [IUserJwtInvalidatedListener.onUserJwtInvalidated]. Delivery occurs on + * a background thread; see [IUserJwtInvalidatedListener]. + */ +class UserJwtInvalidatedEvent( + val externalId: String, +) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 9d34231d63..260a830c81 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -10,6 +10,7 @@ import com.onesignal.core.internal.background.IBackgroundManager import com.onesignal.core.internal.background.impl.BackgroundManager import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.config.impl.ConfigModelStoreListener +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.DatabaseProvider import com.onesignal.core.internal.device.IDeviceService @@ -42,6 +43,7 @@ import com.onesignal.location.ILocationManager import com.onesignal.location.internal.MisconfiguredLocationManager import com.onesignal.notifications.INotificationsManager import com.onesignal.notifications.internal.MisconfiguredNotificationsManager +import com.onesignal.user.internal.identity.JwtTokenStore internal class CoreModule : IModule { override fun register(builder: ServiceBuilder) { @@ -63,6 +65,10 @@ internal class CoreModule : IModule { builder.register().provides() builder.register().provides() + // Identity Verification + builder.register().provides() + builder.register().provides() + // Operations builder.register().provides() builder.register() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt index ec0af86055..273946ef0b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt @@ -84,8 +84,7 @@ internal class ParamsBackendService( return ParamsObject( googleProjectNumber = responseJson.safeString("android_sender_id"), enterprise = responseJson.safeBool("enterp"), - // TODO: New - useIdentityVerification = responseJson.safeBool("require_ident_auth"), + useIdentityVerification = responseJson.safeBool("jwt_required") ?: false, notificationChannels = responseJson.optJSONArray("chnl_lst"), firebaseAnalytics = responseJson.safeBool("fba"), restoreTTLFilter = responseJson.safeBool("restore_ttl_filter"), @@ -95,7 +94,6 @@ internal class ParamsBackendService( unsubscribeWhenNotificationsDisabled = responseJson.safeBool("unsubscribe_on_notifications_disabled"), locationShared = responseJson.safeBool("location_shared"), requiresUserPrivacyConsent = responseJson.safeBool("requires_user_privacy_consent"), - // TODO: New opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"), features = features, influenceParams = influenceParams ?: InfluenceParamsObject(), diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt index a88739e05e..86ac5f56bf 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt @@ -237,12 +237,15 @@ class ConfigModel : Model() { } /** - * Whether SMS auth hash should be used. + * Whether identity verification (JWT) is required for this application. + * - `null` = unknown (remote params haven't arrived yet; all operations are held) + * - `false` = explicitly disabled (SDK behaves as today, no JWT gating) + * - `true` = enabled (operations require a valid JWT, anonymous users are blocked) */ - var useIdentityVerification: Boolean - get() = getBooleanProperty(::useIdentityVerification.name) { false } + var useIdentityVerification: Boolean? + get() = getOptBooleanProperty(::useIdentityVerification.name) set(value) { - setBooleanProperty(::useIdentityVerification.name, value) + setOptBooleanProperty(::useIdentityVerification.name, value) } /** diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/IdentityVerificationService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/IdentityVerificationService.kt new file mode 100644 index 0000000000..597cd908f6 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/IdentityVerificationService.kt @@ -0,0 +1,69 @@ +package com.onesignal.core.internal.config.impl + +import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler +import com.onesignal.common.modeling.ModelChangeTags +import com.onesignal.common.modeling.ModelChangedArgs +import com.onesignal.core.internal.config.ConfigModel +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.operations.IOperationRepo +import com.onesignal.core.internal.startup.IStartableService +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.user.internal.UserManager +import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.identity.JwtTokenStore + +/** + * Reacts to the identity-verification remote param arriving via config HYDRATE. + * + * - When IV transitions from unknown (null) to true: purges anonymous operations. + * - When IV transitions from unknown (null) to any value: wakes the operation queue. + * - On beta migration: if IV=true and the current user has an externalId but no JWT, + * fires [UserJwtInvalidatedEvent] so the developer provides a fresh token. + */ +internal class IdentityVerificationService( + private val _configModelStore: ConfigModelStore, + private val _operationRepo: IOperationRepo, + private val _identityModelStore: IdentityModelStore, + private val _jwtTokenStore: JwtTokenStore, + private val _userManager: UserManager, +) : IStartableService, ISingletonModelStoreChangeHandler { + override fun start() { + _configModelStore.subscribe(this) + _operationRepo.setJwtInvalidatedHandler { externalId -> + _userManager.fireJwtInvalidated(externalId) + } + } + + override fun onModelReplaced( + model: ConfigModel, + tag: String, + ) { + if (tag != ModelChangeTags.HYDRATE) return + + val useIV = model.useIdentityVerification + + var jwtInvalidatedExternalId: String? = null + if (useIV == true) { + Logging.debug("IdentityVerificationService: IV enabled, purging anonymous operations") + _operationRepo.removeOperationsWithoutExternalId() + + val externalId = _identityModelStore.model.externalId + if (externalId != null && _jwtTokenStore.getJwt(externalId) == null) { + Logging.debug("IdentityVerificationService: IV enabled but no JWT for $externalId, will fire invalidated event after queue wake") + jwtInvalidatedExternalId = externalId + } + } + + _operationRepo.forceExecuteOperations() + + jwtInvalidatedExternalId?.let { _userManager.fireJwtInvalidated(it) } + } + + override fun onModelUpdated( + args: ModelChangedArgs, + tag: String, + ) { + // Individual property updates are not expected for remote params; + // ConfigModelStoreListener replaces the entire model on HYDRATE. + } +} 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..22a2d4c517 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,18 +159,6 @@ 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 - if (headers?.cacheKey != null) { val eTag = _prefs.getString( @@ -195,6 +183,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.filterKeys { it != "Authorization" }) + + 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/core/internal/operations/IOperationRepo.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt index d2dceea5c3..84b047e0cf 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationRepo.kt @@ -42,6 +42,24 @@ interface IOperationRepo { suspend fun awaitInitialized() fun forceExecuteOperations() + + /** + * Remove all queued operations that have no externalId (anonymous operations). + * Used by IdentityVerificationService when identity verification is enabled to + * purge operations that cannot be executed without an authenticated user. + */ + fun removeOperationsWithoutExternalId() + + /** + * Register a handler to be called when a runtime 401 Unauthorized response + * invalidates a JWT. This allows the caller to notify the developer so they + * can supply a fresh token via [OneSignal.updateUserJwt]. + * + * The handler is invoked synchronously on the operation repo thread immediately + * after JWT invalidation and re-queue. It must return quickly; defer heavy work + * to another thread. The SDK default handler only schedules listener delivery. + */ + fun setJwtInvalidatedHandler(handler: ((String) -> Unit)?) } // Extension function so the syntax containsInstanceOf() can be used over diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/Operation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/Operation.kt index 76f51994ab..76dc5ab837 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/Operation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/Operation.kt @@ -16,6 +16,18 @@ abstract class Operation(name: String) : Model() { setStringProperty(::name.name, value) } + /** + * The external ID of the user this operation belongs to. Used by [IOperationRepo] to look up + * the correct JWT when identity verification is enabled, and to gate anonymous operations. + * Must be set by each concrete [Operation] subclass constructor — typically from the current + * identity model's externalId at the time the operation is created. + */ + var externalId: String? + get() = getOptStringProperty(::externalId.name) + set(value) { + setOptStringProperty(::externalId.name, value) + } + init { this.name = name } @@ -49,6 +61,13 @@ abstract class Operation(name: String) : Model() { */ abstract val canStartExecute: Boolean + /** + * Whether this operation requires a valid JWT when identity verification is enabled. + * Override to return `false` for operations whose backend endpoint does not require + * a JWT (e.g. subscription updates). + */ + open val requiresJwt: Boolean get() = true + /** * Called when an operation has resolved a local ID to a backend ID (i.e. successfully * created a backend resource). Any IDs within the operation that could be local IDs should diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt index 9b39566d17..4f37c846d6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt @@ -11,6 +11,9 @@ import com.onesignal.core.internal.startup.IStartableService import com.onesignal.core.internal.time.ITime import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging +import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.identity.JwtTokenStore +import com.onesignal.user.internal.operations.LoginUserOperation import com.onesignal.user.internal.operations.impl.states.NewRecordsState import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -28,6 +31,8 @@ internal class OperationRepo( private val _configModelStore: ConfigModelStore, private val _time: ITime, private val _newRecordState: NewRecordsState, + private val _jwtTokenStore: JwtTokenStore, + private val _identityModelStore: IdentityModelStore, ) : IOperationRepo, IStartableService { internal class OperationQueueItem( val operation: Operation, @@ -40,6 +45,9 @@ internal class OperationRepo( } } + @Volatile + private var _jwtInvalidatedHandler: ((String) -> Unit)? = null + internal class LoopWaiterMessage( val force: Boolean, val previousWaitedTime: Long = 0, @@ -120,6 +128,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() @@ -132,6 +142,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() @@ -188,7 +200,8 @@ internal class OperationRepo( } val ops = getNextOps(executeBucket) - Logging.debug("processQueueForever:ops:\n$ops") + val queueSnapshot = synchronized(queue) { queue.toList() } + Logging.debug("processQueueForever:ops:\n$ops\nqueue(${queueSnapshot.size}):\n$queueSnapshot") if (ops != null) { executeOperations(ops) @@ -239,6 +252,13 @@ internal class OperationRepo( } } + private fun dispatchJwtInvalidatedToApp(externalId: String) { + _jwtInvalidatedHandler?.let { handler -> + runCatching { handler(externalId) } + .onFailure { Logging.warn("Failed to run JWT invalidated handler for externalId=$externalId", it) } + } + } + internal suspend fun executeOperations(ops: List) { try { val startingOp = ops.first() @@ -268,7 +288,29 @@ internal class OperationRepo( ops.forEach { _operationModelStore.remove(it.operation.id) } ops.forEach { it.waiter?.wake(true) } } - ExecutionResult.FAIL_UNAUTHORIZED, // TODO: Need to provide callback for app to reset JWT. For now, fail with no retry. + ExecutionResult.FAIL_UNAUTHORIZED -> { + val identityVerificationEnabled = _configModelStore.model.useIdentityVerification == true + val externalId = startingOp.operation.externalId + if (identityVerificationEnabled && externalId != null) { + _jwtTokenStore.invalidateJwt(externalId) + Logging.warn("Operation execution failed with 401 Unauthorized, JWT invalidated for user: $externalId. Operations re-queued.") + // Unblock any enqueueAndWait callers so loginSuspend doesn't hang. + ops.forEach { it.waiter?.wake(false) } + // Re-queue with waiter = null: the operation is preserved for retry + // (once a new JWT is provided via updateUserJwt), but the original + // waiter is detached since it was already woken above. + synchronized(queue) { + ops.reversed().forEach { + queue.add(0, OperationQueueItem(it.operation, waiter = null, bucket = it.bucket, retries = it.retries)) + } + } + dispatchJwtInvalidatedToApp(externalId) + } else { + Logging.warn("Operation execution failed with 401 Unauthorized for anonymous user. Operations dropped.") + ops.forEach { _operationModelStore.remove(it.operation.id) } + ops.forEach { it.waiter?.wake(false) } + } + } ExecutionResult.FAIL_NORETRY, ExecutionResult.FAIL_CONFLICT, -> { @@ -302,9 +344,15 @@ internal class OperationRepo( Logging.error("Operation execution failed with eventual retry, pausing the operation repo: $operations") // keep the failed operation and pause the operation repo from executing paused = true - // add back all operations to the front of the queue to be re-executed. + // Unblock any enqueueAndWait callers so loginSuspend doesn't hang. + ops.forEach { it.waiter?.wake(false) } + // Re-queue with waiter = null: the operation is preserved for retry + // on next cold start, but the original waiter is detached since it + // was already woken above. synchronized(queue) { - ops.reversed().forEach { queue.add(0, it) } + ops.reversed().forEach { + queue.add(0, OperationQueueItem(it.operation, waiter = null, bucket = it.bucket, retries = it.retries)) + } } } } @@ -372,12 +420,16 @@ internal class OperationRepo( } internal fun getNextOps(bucketFilter: Int): List? { + val iv = _configModelStore.model.useIdentityVerification + if (iv == null) return null + return synchronized(queue) { val startingOp = queue.firstOrNull { it.operation.canStartExecute && _newRecordState.canAccess(it.operation.applyToRecordId) && - it.bucket <= bucketFilter + it.bucket <= bucketFilter && + hasValidJwtIfRequired(iv, it.operation) } if (startingOp != null) { @@ -389,6 +441,40 @@ internal class OperationRepo( } } + /** + * Drop anonymous operations at enqueue time when IV is enabled. + * LoginUserOperation is exempt — it's enqueued intentionally during logout + * and purged later by [removeOperationsWithoutExternalId] if needed. + */ + private fun shouldSuppressAnonymousOp(op: Operation): Boolean { + if (op is LoginUserOperation) return false + return _configModelStore.model.useIdentityVerification == true && op.externalId == null + } + + /** + * Determines whether [op] is allowed to execute given the current identity + * verification (IV) state. Used by [getNextOps] to skip operations that + * cannot yet be authenticated. + * + * Returns true (allow) when any of: + * - IV is disabled for this app + * - The operation opts out of JWT gating ([Operation.requiresJwt] = false) + * - A valid JWT is stored for the operation's [Operation.externalId] + * + * Returns false (hold) when IV is enabled and no valid JWT is available, + * which keeps the operation in the queue until the developer supplies one + * via [OneSignal.updateUserJwt]. Anonymous operations (null externalId) are + * also held because they cannot be authenticated. + */ + private fun hasValidJwtIfRequired( + identityVerificationEnabled: Boolean, + op: Operation, + ): Boolean { + if (!identityVerificationEnabled || !op.requiresJwt) return true + val externalId = op.externalId ?: return false + return _jwtTokenStore.getJwt(externalId) != null + } + /** * Given a starting operation, find and remove from the queue all other operations that * can be executed along with the starting operation. The full list of operations, with @@ -450,6 +536,42 @@ internal class OperationRepo( index = 0, ) } + + val activeExternalIds = + synchronized(queue) { + queue.mapNotNull { it.operation.externalId }.toMutableSet() + } + _identityModelStore.model.externalId?.let { activeExternalIds.add(it) } + _jwtTokenStore.pruneToExternalIds(activeExternalIds) + initialized.complete(Unit) } + + override fun removeOperationsWithoutExternalId() { + synchronized(queue) { + val toRemove = queue.filter { it.operation.externalId == null } + toRemove.forEach { + queue.remove(it) + _operationModelStore.remove(it.operation.id) + it.waiter?.wake(false) + } + if (toRemove.isNotEmpty()) { + Logging.debug("OperationRepo: removed ${toRemove.size} anonymous operations (no externalId)") + } + + // IV=ON never transfers anonymous state; clear existingOnesignalId so + // the executor takes the createUser (upsert) path. + queue.forEach { + val op = it.operation + if (op is LoginUserOperation && op.existingOnesignalId != null) { + Logging.debug("OperationRepo: cleared existingOnesignalId on LoginUserOperation (was ${op.existingOnesignalId})") + op.existingOnesignalId = null + } + } + } + } + + override fun setJwtInvalidatedHandler(handler: ((String) -> Unit)?) { + _jwtInvalidatedHandler = handler + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt index f4d4b92a5d..0c9f47c517 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt @@ -272,6 +272,13 @@ object PreferenceOneSignalKeys { */ const val PREFS_OS_IAM_LAST_DISMISSED_TIME = "PREFS_OS_IAM_LAST_DISMISSED_TIME" + // Identity Verification + + /** + * (String) JSON map of externalId -> JWT token for identity verification. + */ + const val PREFS_OS_JWT_TOKENS = "PREFS_OS_JWT_TOKENS" + // Models /** diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/purchases/impl/TrackGooglePurchase.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/purchases/impl/TrackGooglePurchase.kt index 35f95fac5f..8b2694bcb7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/purchases/impl/TrackGooglePurchase.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/purchases/impl/TrackGooglePurchase.kt @@ -240,6 +240,7 @@ internal class TrackGooglePurchase( TrackPurchaseOperation( _configModelStore.model.appId, _identityModelStore.model.onesignalId, + _identityModelStore.model.externalId, newAsExisting, BigDecimal(0), purchasesToReport, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index 5cd9cb9177..e4eed6488e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -2,6 +2,7 @@ package com.onesignal.internal import android.content.Context import com.onesignal.IOneSignal +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.common.AndroidUtils import com.onesignal.common.DeviceUtils import com.onesignal.common.OneSignalUtils @@ -35,8 +36,10 @@ import com.onesignal.user.IUserManager import com.onesignal.user.UserModule import com.onesignal.user.internal.LoginHelper import com.onesignal.user.internal.LogoutHelper +import com.onesignal.user.internal.UserManager import com.onesignal.user.internal.UserSwitcher import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.properties.PropertiesModelStore import com.onesignal.user.internal.resolveAppId import com.onesignal.user.internal.subscriptions.SubscriptionModelStore @@ -142,6 +145,7 @@ internal class OneSignalImp( private val propertiesModelStore: PropertiesModelStore by lazy { services.getService() } private val subscriptionModelStore: SubscriptionModelStore by lazy { services.getService() } private val preferencesService: IPreferencesService by lazy { services.getService() } + private val jwtTokenStore: JwtTokenStore by lazy { services.getService() } private val listOfModules = listOf( "com.onesignal.notifications.NotificationsModule", @@ -220,6 +224,7 @@ internal class OneSignalImp( userSwitcher = userSwitcher, operationRepo = operationRepo, configModel = configModel, + jwtTokenStore = jwtTokenStore, lock = loginLogoutLock, ) } @@ -230,6 +235,7 @@ internal class OneSignalImp( userSwitcher = userSwitcher, operationRepo = operationRepo, configModel = configModel, + subscriptionModelStore = subscriptionModelStore, lock = loginLogoutLock, ) } @@ -374,18 +380,24 @@ internal class OneSignalImp( externalId: String, jwtBearerToken: String?, ) { - Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)") + Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})") if (isBackgroundThreadingEnabled) { waitForInit(operationName = "login") - suspendifyOnIO { loginHelper.login(externalId, jwtBearerToken) } } else { if (!isInitialized) { throw IllegalStateException("Must call 'initWithContext' before 'login'") } + } + + val context = loginHelper.switchUser(externalId, jwtBearerToken) ?: return + + if (isBackgroundThreadingEnabled) { + suspendifyOnIO { loginHelper.enqueueLogin(context) } + } else { Thread { runBlocking(runtimeIoDispatcher) { - loginHelper.login(externalId, jwtBearerToken) + loginHelper.enqueueLogin(context) } }.start() } @@ -409,6 +421,49 @@ internal class OneSignalImp( } } + override fun updateUserJwt( + externalId: String, + token: String, + ) { + Logging.log(LogLevel.DEBUG, "updateUserJwt(externalId: $externalId, token: ...${token.takeLast(8)})") + + if (isBackgroundThreadingEnabled) { + waitForInit(operationName = "updateUserJwt") + jwtTokenStore.putJwt(externalId, token) + operationRepo.forceExecuteOperations() + } else { + if (!isInitialized) { + throw IllegalStateException("Must call 'initWithContext' before 'updateUserJwt'") + } + jwtTokenStore.putJwt(externalId, token) + operationRepo.forceExecuteOperations() + } + } + + override suspend fun updateUserJwtSuspend( + externalId: String, + token: String, + ) = withContext(runtimeIoDispatcher) { + Logging.log(LogLevel.DEBUG, "updateUserJwtSuspend(externalId: $externalId, token: ...${token.takeLast(8)})") + + suspendUntilInit(operationName = "updateUserJwt") + + if (!isInitialized) { + throw IllegalStateException("Must call 'initWithContext' before 'updateUserJwt'") + } + + jwtTokenStore.putJwt(externalId, token) + operationRepo.forceExecuteOperations() + } + + override fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + services.getService().addJwtInvalidatedListener(listener) + } + + override fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + services.getService().removeJwtInvalidatedListener(listener) + } + override fun hasService(c: Class): Boolean = services.hasService(c) override fun getService(c: Class): T = services.getService(c) @@ -638,7 +693,7 @@ internal class OneSignalImp( externalId: String, jwtBearerToken: String?, ) = withContext(runtimeIoDispatcher) { - Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)") + Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})") suspendUntilInit(operationName = "login") @@ -646,7 +701,8 @@ internal class OneSignalImp( throw IllegalStateException("'initWithContext failed' before 'login'") } - loginHelper.login(externalId, jwtBearerToken) + val context = loginHelper.switchUser(externalId, jwtBearerToken) ?: return@withContext + loginHelper.enqueueLogin(context) } override suspend fun logoutSuspend() = diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt index 1fcdcd8641..4f2ee4959c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt @@ -44,7 +44,7 @@ internal class SessionListener( override fun onSessionStarted() { _propertiesModelStore.model.timezone = TimeUtils.getTimeZoneId() - _operationRepo.enqueue(TrackSessionStartOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId), true) + _operationRepo.enqueue(TrackSessionStartOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId, _identityModelStore.model.externalId), true) } override fun onSessionActive() { @@ -60,7 +60,7 @@ internal class SessionListener( } _operationRepo.enqueue( - TrackSessionEndOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId, durationInSeconds), + TrackSessionEndOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId, _identityModelStore.model.externalId, durationInSeconds), ) suspendifyOnIO { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt index be55228756..15a1e9dc96 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt @@ -75,7 +75,9 @@ internal class UserModule : IModule { builder.register().provides() builder.register().provides() builder.register().provides() - builder.register().provides() + builder.register() + .provides() + .provides() builder.register().provides() builder.register().provides() builder.register().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LoginHelper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LoginHelper.kt index 15939441ba..ef516d06a7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LoginHelper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LoginHelper.kt @@ -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.identity.JwtTokenStore import com.onesignal.user.internal.operations.LoginUserOperation class LoginHelper( @@ -11,39 +12,65 @@ class LoginHelper( private val userSwitcher: UserSwitcher, private val operationRepo: IOperationRepo, private val configModel: ConfigModel, + private val jwtTokenStore: JwtTokenStore, private val lock: Any, ) { - suspend fun login( + internal data class LoginEnqueueContext( + val appId: String, + val newIdentityOneSignalId: String, + val externalId: String, + val existingOneSignalId: String?, + ) + + /** + * Synchronously switches local user models under the login/logout lock. + * Returns context needed for [enqueueLogin], or null if the user was + * already logged in with [externalId] (no switch needed). + */ + internal fun switchUser( externalId: String, jwtBearerToken: String? = null, - ) { - var currentIdentityExternalId: String? = null - var currentIdentityOneSignalId: String? = null - var newIdentityOneSignalId: String = "" - + ): LoginEnqueueContext? { synchronized(lock) { - currentIdentityExternalId = identityModelStore.model.externalId - currentIdentityOneSignalId = identityModelStore.model.onesignalId + val currentExternalId = identityModelStore.model.externalId + val currentOneSignalId = identityModelStore.model.onesignalId - if (currentIdentityExternalId == externalId) { - return + if (currentExternalId == externalId) { + jwtTokenStore.putJwt(externalId, jwtBearerToken) + operationRepo.forceExecuteOperations() + return null } - // TODO: Set JWT Token for all future requests. + jwtTokenStore.putJwt(externalId, jwtBearerToken) + userSwitcher.createAndSwitchToNewUser { identityModel, _ -> identityModel.externalId = externalId } - newIdentityOneSignalId = identityModelStore.model.onesignalId + val newOneSignalId = identityModelStore.model.onesignalId + + val existingOneSignalId = + if (configModel.useIdentityVerification == true) { + null + } else { + if (currentExternalId == null) currentOneSignalId else null + } + + return LoginEnqueueContext(configModel.appId, newOneSignalId, externalId, existingOneSignalId) } + } + /** + * Enqueues the [LoginUserOperation] and suspends until it completes. + */ + internal suspend fun enqueueLogin(context: LoginEnqueueContext) { val result = operationRepo.enqueueAndWait( LoginUserOperation( - configModel.appId, - newIdentityOneSignalId, - externalId, - if (currentIdentityExternalId == null) currentIdentityOneSignalId else null, + context.appId, + context.newIdentityOneSignalId, + context.externalId, + context.existingOneSignalId, ), ) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LogoutHelper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LogoutHelper.kt index 8d9015c612..6290945074 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LogoutHelper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LogoutHelper.kt @@ -4,12 +4,14 @@ import com.onesignal.core.internal.config.ConfigModel 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( private val identityModelStore: IdentityModelStore, private val userSwitcher: UserSwitcher, private val operationRepo: IOperationRepo, private val configModel: ConfigModel, + private val subscriptionModelStore: SubscriptionModelStore, private val lock: Any, ) { fun logout() { @@ -18,20 +20,41 @@ class LogoutHelper( return } - // Create new device-scoped user (clears external ID) - userSwitcher.createAndSwitchToNewUser() + if (configModel.useIdentityVerification == true) { + configModel.pushSubscriptionId?.let { pushSubId -> + subscriptionModelStore.get(pushSubId) + ?.let { it.isDisabledInternally = true } + } - // Enqueue login operation for the new device-scoped user (no external ID) - operationRepo.enqueue( - LoginUserOperation( - configModel.appId, - identityModelStore.model.onesignalId, - null, - // No external ID for device-scoped user - ), - ) + userSwitcher.createAndSwitchToNewUser(suppressBackendOperation = true) + } else if (configModel.useIdentityVerification == false) { + userSwitcher.createAndSwitchToNewUser() - // TODO: remove JWT Token for all future requests. + operationRepo.enqueue( + LoginUserOperation( + configModel.appId, + identityModelStore.model.onesignalId, + null, + ), + ) + } else { + // IV unknown (pre-HYDRATE): disable push, enqueue anonymous user. + // If IV=ON at HYDRATE, removeOperationsWithoutExternalId() purges these. + configModel.pushSubscriptionId?.let { pushSubId -> + subscriptionModelStore.get(pushSubId) + ?.let { it.isDisabledInternally = true } + } + + userSwitcher.createAndSwitchToNewUser() + + operationRepo.enqueue( + LoginUserOperation( + configModel.appId, + identityModelStore.model.onesignalId, + null, + ), + ) + } } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index 328cb9da7d..e55e03ff63 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -1,5 +1,7 @@ package com.onesignal.user.internal +import com.onesignal.IUserJwtInvalidatedListener +import com.onesignal.UserJwtInvalidatedEvent import com.onesignal.common.IDManager import com.onesignal.common.JSONUtils import com.onesignal.common.OneSignalUtils @@ -22,6 +24,10 @@ import com.onesignal.user.state.IUserStateObserver import com.onesignal.user.state.UserChangedState import com.onesignal.user.state.UserState import com.onesignal.user.subscriptions.IPushSubscription +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch internal open class UserManager( private val _subscriptionManager: ISubscriptionManager, @@ -43,6 +49,35 @@ internal open class UserManager( get() = _subscriptionManager.subscriptions val changeHandlersNotifier = EventProducer() + private val jwtInvalidatedNotifier = EventProducer() + + // Coroutine scope for async JWT invalidated listener delivery (non-blocking) + private val jwtInvalidatedAppCallbackScope = + CoroutineScope(SupervisorJob() + Dispatchers.Default) + + fun addJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + jwtInvalidatedNotifier.subscribe(listener) + } + + fun removeJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { + jwtInvalidatedNotifier.unsubscribe(listener) + } + + /** + * Schedules [IUserJwtInvalidatedListener] delivery on a background dispatcher so HYDRATE and + * operation-repo paths can finish internal work before app code runs. + */ + fun fireJwtInvalidated(externalId: String) { + jwtInvalidatedAppCallbackScope.launch { + runCatching { + jwtInvalidatedNotifier.fire { listener -> + listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(externalId)) + } + }.onFailure { + Logging.warn("Failed to deliver JWT invalidated event for externalId=$externalId", it) + } + } + } override val pushSubscription: IPushSubscription get() = _subscriptionManager.subscriptions.push diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserSwitcher.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserSwitcher.kt index 5fba367b1a..f1401e031a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserSwitcher.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserSwitcher.kt @@ -173,6 +173,7 @@ class UserSwitcher( LoginUserFromSubscriptionOperation( configModel.appId, identityModelStore.model.onesignalId, + identityModelStore.model.externalId, legacyPlayerId, ), ) 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..a09f40ca68 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 @@ -1,6 +1,7 @@ package com.onesignal.user.internal.backend import com.onesignal.common.exceptions.BackendException +import com.onesignal.debug.internal.logging.Logging interface IIdentityBackendService { /** @@ -18,6 +19,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, identities: Map, + jwt: String? = null, ): Map /** @@ -35,6 +37,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String? = null, ) } @@ -48,4 +51,23 @@ object IdentityConstants { * The alias label for the internal onesignal ID alias. */ const val ONESIGNAL_ID = "onesignal_id" + + /** + * Resolves which alias (external_id vs onesignal_id) should be used in backend API paths. + * When identity verification is enabled and the operation has an externalId, routes through + * external_id; otherwise falls back to onesignal_id. + */ + fun resolveAlias( + useIdentityVerification: Boolean?, + externalId: String?, + onesignalId: String, + ): Pair { + if (useIdentityVerification == true) { + if (externalId != null) { + return EXTERNAL_ID to externalId + } + Logging.error("Identity verification is enabled but externalId is null. Falling back to onesignal_id.") + } + return ONESIGNAL_ID to onesignalId + } } 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..e6e65bff1f 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,6 +64,7 @@ interface ISubscriptionBackendService { subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String? = null, ) /** @@ -74,5 +78,6 @@ interface ISubscriptionBackendService { suspend fun getIdentityFromSubscription( appId: String, subscriptionId: String, + jwt: String? = null, ): Map } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt index 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..614b8a3bf3 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,13 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, identities: Map, + jwt: String?, ): Map { val requestJSONObject = JSONObject() .put("identity", JSONObject().putMap(identities)) - val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject) + val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject, jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -36,8 +38,9 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete") + val response = _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete", jwt?.let { OptionalHeaders(jwt = it) }) 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..1003dd84c5 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,12 @@ 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, jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -50,12 +52,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, jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -76,8 +79,9 @@ internal class SubscriptionBackendService( override suspend fun deleteSubscription( appId: String, subscriptionId: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId") + val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId", jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -89,12 +93,13 @@ internal class SubscriptionBackendService( subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String?, ) { val requestJSON = JSONObject() .put("identity", JSONObject().put(aliasLabel, aliasValue)) - val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON) + val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON, jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -104,8 +109,9 @@ internal class SubscriptionBackendService( override suspend fun getIdentityFromSubscription( appId: String, subscriptionId: String, + jwt: String?, ): Map { - val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity") + val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity", jwt?.let { OptionalHeaders(jwt = it) }) 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..8a5c58d691 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, jwt?.let { OptionalHeaders(jwt = it) }) 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, jwt?.let { OptionalHeaders(jwt = it) }) 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", jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/builduser/impl/RebuildUserService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/builduser/impl/RebuildUserService.kt index a9f42bcfe1..22442072e5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/builduser/impl/RebuildUserService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/builduser/impl/RebuildUserService.kt @@ -52,6 +52,7 @@ class RebuildUserService( CreateSubscriptionOperation( appId, onesignalId, + identityModel.externalId, pushSubscription.id, pushSubscription.type, pushSubscription.optedIn, @@ -60,7 +61,7 @@ class RebuildUserService( ), ) } - operations.add(RefreshUserOperation(appId, onesignalId)) + operations.add(RefreshUserOperation(appId, onesignalId, identityModel.externalId)) return operations } } 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..eccd67b650 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, jwt?.let { OptionalHeaders(jwt = it) }) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/JwtTokenStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/JwtTokenStore.kt new file mode 100644 index 0000000000..c678d6d219 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/JwtTokenStore.kt @@ -0,0 +1,106 @@ +package com.onesignal.user.internal.identity + +import com.onesignal.core.internal.preferences.IPreferencesService +import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys +import com.onesignal.core.internal.preferences.PreferenceStores +import com.onesignal.debug.internal.logging.Logging +import org.json.JSONException +import org.json.JSONObject + +/** + * Persistent store mapping externalId -> JWT token. Supports multiple users simultaneously + * so that queued operations for a previous user can still resolve their JWT at execution time. + * + * Storage is unconditional (callers store JWTs regardless of the identity-verification flag). + * Only *usage* of JWTs (Authorization header, gating, alias resolution) is gated on + * [com.onesignal.core.internal.config.ConfigModel.useIdentityVerification]. + */ +class JwtTokenStore( + private val _prefs: IPreferencesService, +) { + private val tokens: MutableMap = mutableMapOf() + private var isLoaded = false + + /** Not thread-safe; callers must hold `synchronized(tokens)`. */ + private fun ensureLoaded() { + if (isLoaded) return + val json = + _prefs.getString( + PreferenceStores.ONESIGNAL, + PreferenceOneSignalKeys.PREFS_OS_JWT_TOKENS, + ) + if (json != null) { + try { + val obj = JSONObject(json) + for (key in obj.keys()) { + tokens[key] = obj.getString(key) + } + } catch (e: JSONException) { + Logging.warn("JwtTokenStore: failed to parse persisted tokens, starting fresh", e) + } + } + isLoaded = true + } + + /** Not thread-safe; callers must hold `synchronized(tokens)`. */ + private fun persist() { + _prefs.saveString( + PreferenceStores.ONESIGNAL, + PreferenceOneSignalKeys.PREFS_OS_JWT_TOKENS, + JSONObject(tokens.toMap()).toString(), + ) + } + + /** + * Returns the JWT for the given [externalId], or null if none is stored. + */ + fun getJwt(externalId: String): String? { + synchronized(tokens) { + ensureLoaded() + return tokens[externalId] + } + } + + /** + * Stores (or replaces) the JWT for [externalId]. Passing a null [jwt] is a no-op; + * use [invalidateJwt] to remove a token. + */ + fun putJwt( + externalId: String, + jwt: String?, + ) { + if (jwt == null) return + synchronized(tokens) { + ensureLoaded() + tokens[externalId] = jwt + persist() + } + } + + /** + * Removes the JWT for [externalId], marking it as invalid. Operations for this user + * will be held until a new JWT is provided via [putJwt]. + */ + fun invalidateJwt(externalId: String) { + synchronized(tokens) { + ensureLoaded() + if (tokens.remove(externalId) != null) { + persist() + } + } + } + + /** + * Removes all stored JWTs whose externalId is NOT in [activeIds]. + * Called on cold start after loading persisted operations to prevent unbounded growth. + */ + fun pruneToExternalIds(activeIds: Set) { + synchronized(tokens) { + ensureLoaded() + val removed = tokens.keys.retainAll(activeIds) + if (removed) { + persist() + } + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/CreateSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/CreateSubscriptionOperation.kt index 35484f670d..c1335a1b25 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/CreateSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/CreateSubscriptionOperation.kt @@ -88,9 +88,10 @@ class CreateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.CR override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.subscriptionId = subscriptionId this.type = type this.enabled = enabled diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt index 1595a6de2b..cfb1bf0bac 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt @@ -43,9 +43,10 @@ class DeleteAliasOperation() : Operation(IdentityOperationExecutor.DELETE_ALIAS) override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, label: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, label: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.label = label } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteSubscriptionOperation.kt index 14c9aee448..8a5cc6bb52 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteSubscriptionOperation.kt @@ -44,9 +44,10 @@ class DeleteSubscriptionOperation() : Operation(SubscriptionOperationExecutor.DE override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(subscriptionId) override val applyToRecordId: String get() = subscriptionId - constructor(appId: String, onesignalId: String, subscriptionId: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, subscriptionId: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.subscriptionId = subscriptionId } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteTagOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteTagOperation.kt index f88ae3c568..4819c3248f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteTagOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteTagOperation.kt @@ -44,9 +44,10 @@ class DeleteTagOperation() : Operation(UpdateUserOperationExecutor.DELETE_TAG) { override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, key: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, key: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.key = key } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserFromSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserFromSubscriptionOperation.kt index 9597283f75..655e71777c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserFromSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserFromSubscriptionOperation.kt @@ -42,9 +42,10 @@ class LoginUserFromSubscriptionOperation() : Operation(LoginUserFromSubscription override val canStartExecute: Boolean = true override val applyToRecordId: String get() = subscriptionId - constructor(appId: String, onesignalId: String, subscriptionId: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, subscriptionId: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.subscriptionId = subscriptionId } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt index b283cc3da0..521be948fe 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt @@ -32,15 +32,6 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) { setStringProperty(::onesignalId.name, value) } - /** - * The optional external ID of this newly logged-in user. Must be unique for the [appId]. - */ - var externalId: String? - get() = getOptStringProperty(::externalId.name) - private set(value) { - setOptStringProperty(::externalId.name, value) - } - /** * The user ID of an existing user the [externalId] will be attempted to be associated to first. * When null (or non-null but unsuccessful), a new user will be upserted. This ID *may* be locally generated @@ -48,7 +39,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) { */ var existingOnesignalId: String? get() = getOptStringProperty(::existingOnesignalId.name) - private set(value) { + internal set(value) { setOptStringProperty(::existingOnesignalId.name, value) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/RefreshUserOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/RefreshUserOperation.kt index 953cbe7b9c..5f57cbac79 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/RefreshUserOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/RefreshUserOperation.kt @@ -35,9 +35,10 @@ class RefreshUserOperation() : Operation(RefreshUserOperationExecutor.REFRESH_US override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId } override fun translateIds(map: Map) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt index c88c23e46f..3bf5fd678b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt @@ -53,9 +53,10 @@ class SetAliasOperation() : Operation(IdentityOperationExecutor.SET_ALIAS) { override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, label: String, value: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, label: String, value: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.label = label this.value = value } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetPropertyOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetPropertyOperation.kt index 2aff9c174a..0be614b9eb 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetPropertyOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetPropertyOperation.kt @@ -52,9 +52,10 @@ class SetPropertyOperation() : Operation(UpdateUserOperationExecutor.SET_PROPERT override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, property: String, value: Any?) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, property: String, value: Any?) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.property = property this.value = value } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetTagOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetTagOperation.kt index 88bfa06eda..505b79506c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetTagOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetTagOperation.kt @@ -53,9 +53,10 @@ class SetTagOperation() : Operation(UpdateUserOperationExecutor.SET_TAG) { override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, key: String, value: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, key: String, value: String) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.key = key this.value = value } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt index b510a4fd3f..04956e1877 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt @@ -30,15 +30,6 @@ class TrackCustomEventOperation() : Operation(CustomEventOperationExecutor.CUSTO setStringProperty(::onesignalId.name, value) } - /** - * The optional external ID of current logged-in user. Must be unique for the [appId]. - */ - var externalId: String? - get() = getOptStringProperty(::externalId.name) - private set(value) { - setOptStringProperty(::externalId.name, value) - } - /** * The timestamp when the custom event was created. */ diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackPurchaseOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackPurchaseOperation.kt index 78da8cfb0a..7abb7170be 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackPurchaseOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackPurchaseOperation.kt @@ -65,9 +65,10 @@ class TrackPurchaseOperation() : Operation(UpdateUserOperationExecutor.TRACK_PUR override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, treatNewAsExisting: Boolean, amountSpent: BigDecimal, purchases: List) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, treatNewAsExisting: Boolean, amountSpent: BigDecimal, purchases: List) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.treatNewAsExisting = treatNewAsExisting this.amountSpent = amountSpent this.purchases = purchases diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionEndOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionEndOperation.kt index 940051e91b..c0dfce68cc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionEndOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionEndOperation.kt @@ -43,9 +43,10 @@ class TrackSessionEndOperation() : Operation(UpdateUserOperationExecutor.TRACK_S override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, sessionTime: Long) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, sessionTime: Long) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.sessionTime = sessionTime } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionStartOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionStartOperation.kt index e5b9e0f29c..5b1285f5f7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionStartOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionStartOperation.kt @@ -34,9 +34,10 @@ class TrackSessionStartOperation() : Operation(UpdateUserOperationExecutor.TRACK override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String) : this() { + constructor(appId: String, onesignalId: String, externalId: String?) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId } override fun translateIds(map: Map) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TransferSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TransferSubscriptionOperation.kt index 54aa3bae27..c0d227260e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TransferSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TransferSubscriptionOperation.kt @@ -50,10 +50,11 @@ class TransferSubscriptionOperation() : Operation(SubscriptionOperationExecutor. override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(subscriptionId) override val applyToRecordId: String get() = subscriptionId - constructor(appId: String, subscriptionId: String, onesignalId: String) : this() { + constructor(appId: String, subscriptionId: String, onesignalId: String, externalId: String?) : this() { this.appId = appId this.subscriptionId = subscriptionId this.onesignalId = onesignalId + this.externalId = externalId } override fun translateIds(map: Map) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt index 57f17a29f5..9507bdc185 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt @@ -86,10 +86,12 @@ class UpdateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.UP override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(subscriptionId) override val applyToRecordId: String get() = subscriptionId + override val requiresJwt: Boolean get() = false - constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() { this.appId = appId this.onesignalId = onesignalId + this.externalId = externalId this.subscriptionId = subscriptionId this.type = type this.enabled = enabled 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..18e97a1178 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 @@ -13,12 +13,14 @@ 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.identity.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, ) : IOperationExecutor { override val operations: List get() = listOf(CUSTOM_EVENT) @@ -40,6 +42,7 @@ internal class CustomEventOperationExecutor( try { when (operation) { is TrackCustomEventOperation -> { + val jwt = operation.externalId?.let { _jwtTokenStore.getJwt(it) } customEventBackendService.sendCustomEvent( operation.appId, operation.onesignalId, @@ -48,6 +51,7 @@ internal class CustomEventOperationExecutor( operation.eventName, operation.eventProperties, eventMetadataJson, + jwt, ) } } @@ -57,6 +61,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/IdentityOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt index 104fe9569f..f12831c3f9 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,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.ConfigModelStore import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor @@ -12,6 +13,7 @@ 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.identity.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 +23,8 @@ internal class IdentityOperationExecutor( private val _identityModelStore: IdentityModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, + private val _configModelStore: ConfigModelStore, + private val _jwtTokenStore: JwtTokenStore, ) : IOperationExecutor { override val operations: List get() = listOf(SET_ALIAS, DELETE_ALIAS) @@ -44,12 +48,21 @@ internal class IdentityOperationExecutor( val lastOperation = operations.last() if (lastOperation is SetAliasOperation) { + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + lastOperation.externalId, + lastOperation.onesignalId, + ) + val jwt = lastOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + try { _identityBackend.setAlias( lastOperation.appId, - IdentityConstants.ONESIGNAL_ID, - lastOperation.onesignalId, + aliasLabel, + aliasValue, mapOf(lastOperation.label to lastOperation.value), + jwt, ) // ensure the now created alias is in the model as long as the user is still current. @@ -87,12 +100,21 @@ internal class IdentityOperationExecutor( } } } else if (lastOperation is DeleteAliasOperation) { + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + lastOperation.externalId, + lastOperation.onesignalId, + ) + val jwt = lastOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + try { _identityBackend.deleteAlias( lastOperation.appId, - IdentityConstants.ONESIGNAL_ID, - lastOperation.onesignalId, + aliasLabel, + aliasValue, lastOperation.label, + 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 84093eeccb..15072ec014 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.ConfigModelStore 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 _configModelStore: ConfigModelStore, ) : IOperationExecutor { override val operations: List get() = listOf(LOGIN_USER_FROM_SUBSCRIPTION_USER) @@ -27,6 +29,11 @@ internal class LoginUserFromSubscriptionOperationExecutor( override suspend fun execute(operations: List): ExecutionResponse { Logging.debug("LoginUserFromSubscriptionOperationExecutor(operation: $operations)") + if (_configModelStore.model.useIdentityVerification == true) { + 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") } @@ -74,7 +81,7 @@ internal class LoginUserFromSubscriptionOperationExecutor( return ExecutionResponse( ExecutionResult.SUCCESS, idTranslations, - listOf(RefreshUserOperation(loginUserOp.appId, backendOneSignalId)), + listOf(RefreshUserOperation(loginUserOp.appId, backendOneSignalId, loginUserOp.externalId)), ) } catch (ex: BackendException) { val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) 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 46968b3e71..013c343340 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 @@ -24,6 +24,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.identity.JwtTokenStore import com.onesignal.user.internal.operations.CreateSubscriptionOperation import com.onesignal.user.internal.operations.DeleteSubscriptionOperation import com.onesignal.user.internal.operations.LoginUserOperation @@ -47,6 +48,7 @@ internal class LoginUserOperationExecutor( private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, + private val _jwtTokenStore: JwtTokenStore, ) : IOperationExecutor { override val operations: List get() = listOf(LOGIN_USER) @@ -72,6 +74,7 @@ internal class LoginUserOperationExecutor( // Anonymous Login being processed alone will surely be rejected, so we need to drop the request val containsSubscriptionOperation = operations.any { it is CreateSubscriptionOperation || it is TransferSubscriptionOperation } if (!containsSubscriptionOperation && loginUserOp.externalId == null) { + Logging.error("LoginUserOperationExecutor: dropping anonymous LoginUserOperation with no subscription op: $loginUserOp") return ExecutionResponse(ExecutionResult.FAIL_NORETRY) } if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null) { @@ -89,6 +92,7 @@ internal class LoginUserOperationExecutor( SetAliasOperation( loginUserOp.appId, loginUserOp.existingOnesignalId!!, + loginUserOp.externalId, IdentityConstants.EXTERNAL_ID, loginUserOp.externalId!!, ), @@ -168,7 +172,8 @@ internal class LoginUserOperationExecutor( try { val subscriptionList = subscriptions.toList() - val response = _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties) + val jwt = createUserOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + 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. @@ -223,7 +228,7 @@ internal class LoginUserOperationExecutor( val wasPossiblyAnUpsert = identities.isNotEmpty() val followUpOperations = if (wasPossiblyAnUpsert) { - listOf(RefreshUserOperation(createUserOperation.appId, backendOneSignalId)) + listOf(RefreshUserOperation(createUserOperation.appId, backendOneSignalId, createUserOperation.externalId)) } else { null } 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..02e10bbc22 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 @@ -17,6 +17,7 @@ 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.identity.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,7 @@ internal class RefreshUserOperationExecutor( private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, + private val _jwtTokenStore: JwtTokenStore, ) : IOperationExecutor { override val operations: List get() = listOf(REFRESH_USER) @@ -54,12 +56,21 @@ internal class RefreshUserOperationExecutor( } private suspend fun getUser(op: RefreshUserOperation): ExecutionResponse { + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + op.externalId, + op.onesignalId, + ) + val jwt = op.externalId?.let { _jwtTokenStore.getJwt(it) } + try { val response = _userBackend.getUser( op.appId, - IdentityConstants.ONESIGNAL_ID, - op.onesignalId, + aliasLabel, + aliasValue, + 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 81ab0bb687..bea804dd44 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 @@ -26,6 +26,7 @@ import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService +import com.onesignal.user.internal.identity.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,7 @@ internal class SubscriptionOperationExecutor( private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, + private val _jwtTokenStore: JwtTokenStore, ) : IOperationExecutor { override val operations: List get() = listOf(CREATE_SUBSCRIPTION, UPDATE_SUBSCRIPTION, DELETE_SUBSCRIPTION, TRANSFER_SUBSCRIPTION) @@ -107,12 +109,21 @@ internal class SubscriptionOperationExecutor( AndroidUtils.getAppVersion(_applicationService.appContext), ) + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + createOperation.externalId, + createOperation.onesignalId, + ) + val jwt = createOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + val result = _subscriptionBackend.createSubscription( createOperation.appId, - IdentityConstants.ONESIGNAL_ID, - createOperation.onesignalId, + aliasLabel, + aliasValue, subscription, + jwt, ) ?: return ExecutionResponse(ExecutionResult.SUCCESS) val backendSubscriptionId = result.first @@ -190,7 +201,8 @@ internal class SubscriptionOperationExecutor( AndroidUtils.getAppVersion(_applicationService.appContext), ) - val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription) + val jwt = lastOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription, jwt) if (rywData != null) { _consistencyManager.setRywData(startingOperation.onesignalId, IamFetchRywTokenKey.SUBSCRIPTION, rywData) @@ -220,6 +232,7 @@ internal class SubscriptionOperationExecutor( CreateSubscriptionOperation( lastOperation.appId, lastOperation.onesignalId, + lastOperation.externalId, lastOperation.subscriptionId, lastOperation.type, lastOperation.enabled, @@ -239,12 +252,26 @@ 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 { + if (_configModelStore.model.useIdentityVerification == true) { + Logging.warn("TransferSubscriptionOperation is not supported when identity verification is enabled. Dropping.") + return ExecutionResponse(ExecutionResult.FAIL_NORETRY) + } + + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + startingOperation.externalId, + startingOperation.onesignalId, + ) + val jwt = startingOperation.externalId?.let { _jwtTokenStore.getJwt(it) } + try { _subscriptionBackend.transferSubscription( startingOperation.appId, startingOperation.subscriptionId, - IdentityConstants.ONESIGNAL_ID, - startingOperation.onesignalId, + aliasLabel, + aliasValue, + jwt, ) } catch (ex: BackendException) { val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) @@ -275,8 +302,10 @@ internal class SubscriptionOperationExecutor( } private suspend fun deleteSubscription(op: DeleteSubscriptionOperation): ExecutionResponse { + val jwt = op.externalId?.let { _jwtTokenStore.getJwt(it) } + 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) @@ -299,6 +328,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..090b3f3904 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.ConfigModelStore import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor @@ -19,6 +20,7 @@ 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.identity.JwtTokenStore import com.onesignal.user.internal.operations.DeleteTagOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation @@ -35,6 +37,8 @@ internal class UpdateUserOperationExecutor( private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, + private val _configModelStore: ConfigModelStore, + private val _jwtTokenStore: JwtTokenStore, ) : IOperationExecutor { override val operations: List get() = listOf(SET_TAG, DELETE_TAG, SET_PROPERTY, TRACK_SESSION_START, TRACK_SESSION_END, TRACK_PURCHASE) @@ -137,15 +141,25 @@ internal class UpdateUserOperationExecutor( } if (appId != null && onesignalId != null) { + val firstOp = operations.first() + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + firstOp.externalId, + onesignalId, + ) + val jwt = firstOp.externalId?.let { _jwtTokenStore.getJwt(it) } + try { val rywData = _userBackend.updateUser( appId, - IdentityConstants.ONESIGNAL_ID, - onesignalId, + aliasLabel, + aliasValue, propertiesObject, refreshDeviceMetadata, deltasObject, + jwt, ) if (rywData != null) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/IdentityModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/IdentityModelStoreListener.kt index 90a565a5a2..acd1141bb3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/IdentityModelStoreListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/IdentityModelStoreListener.kt @@ -10,10 +10,10 @@ import com.onesignal.user.internal.operations.DeleteAliasOperation import com.onesignal.user.internal.operations.SetAliasOperation internal class IdentityModelStoreListener( - store: IdentityModelStore, + private val _identityModelStore: IdentityModelStore, opRepo: IOperationRepo, private val _configModelStore: ConfigModelStore, -) : SingletonModelStoreListener(store, opRepo) { +) : SingletonModelStoreListener(_identityModelStore, opRepo) { override fun getReplaceOperation(model: IdentityModel): Operation? { // when the identity model is replaced, nothing to do on the backend. Already handled via login process. return null @@ -25,11 +25,11 @@ internal class IdentityModelStoreListener( property: String, oldValue: Any?, newValue: Any?, - ): Operation { + ): Operation? { return if (newValue != null && newValue is String) { - SetAliasOperation(_configModelStore.model.appId, model.onesignalId, property, newValue) + SetAliasOperation(_configModelStore.model.appId, model.onesignalId, model.externalId, property, newValue) } else { - DeleteAliasOperation(_configModelStore.model.appId, model.onesignalId, property) + DeleteAliasOperation(_configModelStore.model.appId, model.onesignalId, model.externalId, property) } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/PropertiesModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/PropertiesModelStoreListener.kt index d020c5cc66..198326b473 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/PropertiesModelStoreListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/PropertiesModelStoreListener.kt @@ -4,6 +4,7 @@ import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.operations.IOperationRepo import com.onesignal.core.internal.operations.Operation import com.onesignal.core.internal.operations.listeners.SingletonModelStoreListener +import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.DeleteTagOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation @@ -14,6 +15,7 @@ internal class PropertiesModelStoreListener( store: PropertiesModelStore, opRepo: IOperationRepo, private val _configModelStore: ConfigModelStore, + private val _identityModelStore: IdentityModelStore, ) : SingletonModelStoreListener(store, opRepo) { override fun getReplaceOperation(model: PropertiesModel): Operation? { // when the property model is replaced, nothing to do on the backend. Already handled via login process. @@ -38,12 +40,12 @@ internal class PropertiesModelStoreListener( if (path.startsWith(PropertiesModel::tags.name)) { return if (newValue != null && newValue is String) { - SetTagOperation(_configModelStore.model.appId, model.onesignalId, property, newValue) + SetTagOperation(_configModelStore.model.appId, model.onesignalId, _identityModelStore.model.externalId, property, newValue) } else { - DeleteTagOperation(_configModelStore.model.appId, model.onesignalId, property) + DeleteTagOperation(_configModelStore.model.appId, model.onesignalId, _identityModelStore.model.externalId, property) } } - return SetPropertyOperation(_configModelStore.model.appId, model.onesignalId, property, newValue) + return SetPropertyOperation(_configModelStore.model.appId, model.onesignalId, _identityModelStore.model.externalId, property, newValue) } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/SubscriptionModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/SubscriptionModelStoreListener.kt index f0002940e9..cfc11987f2 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/SubscriptionModelStoreListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/SubscriptionModelStoreListener.kt @@ -18,11 +18,12 @@ internal class SubscriptionModelStoreListener( private val _identityModelStore: IdentityModelStore, private val _configModelStore: ConfigModelStore, ) : ModelStoreListener(store, opRepo) { - override fun getAddOperation(model: SubscriptionModel): Operation { + override fun getAddOperation(model: SubscriptionModel): Operation? { val enabledAndStatus = getSubscriptionEnabledAndStatus(model) return CreateSubscriptionOperation( _configModelStore.model.appId, _identityModelStore.model.onesignalId, + _identityModelStore.model.externalId, model.id, model.type, enabledAndStatus.first, @@ -31,8 +32,8 @@ internal class SubscriptionModelStoreListener( ) } - override fun getRemoveOperation(model: SubscriptionModel): Operation { - return DeleteSubscriptionOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId, model.id) + override fun getRemoveOperation(model: SubscriptionModel): Operation? { + return DeleteSubscriptionOperation(_configModelStore.model.appId, _identityModelStore.model.onesignalId, _identityModelStore.model.externalId, model.id) } override fun getUpdateOperation( @@ -41,11 +42,12 @@ internal class SubscriptionModelStoreListener( property: String, oldValue: Any?, newValue: Any?, - ): Operation { + ): Operation? { val enabledAndStatus = getSubscriptionEnabledAndStatus(model) return UpdateSubscriptionOperation( _configModelStore.model.appId, _identityModelStore.model.onesignalId, + _identityModelStore.model.externalId, model.id, model.type, enabledAndStatus.first, @@ -56,6 +58,10 @@ internal class SubscriptionModelStoreListener( companion object { fun getSubscriptionEnabledAndStatus(model: SubscriptionModel): Pair { + if (model.isDisabledInternally) { + return Pair(false, SubscriptionStatus.UNSUBSCRIBE) + } + val status: SubscriptionStatus val enabled: Boolean diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/service/UserRefreshService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/service/UserRefreshService.kt index 7b04d7981e..98e6f719db 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/service/UserRefreshService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/service/UserRefreshService.kt @@ -32,6 +32,7 @@ class UserRefreshService( RefreshUserOperation( _configModelStore.model.appId, _identityModelStore.model.onesignalId, + _identityModelStore.model.externalId, ), ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/subscriptions/SubscriptionModel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/subscriptions/SubscriptionModel.kt index c7bde3aae8..a4622d3aee 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/subscriptions/SubscriptionModel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/subscriptions/SubscriptionModel.kt @@ -92,6 +92,20 @@ class SubscriptionModel : Model() { setBooleanProperty(::optedIn.name, value) } + /** + * Set to true by the SDK when logout is called with Identity Verification enabled. + * The real [optedIn] and [status] remain unchanged to preserve the user's preference. + * When a subscription update is built, this flag causes enabled=false and + * status=UNSUBSCRIBE to be sent to the backend instead of the real values. + * On the next login, [UserSwitcher.createAndSwitchToNewUser] creates a fresh model + * that does not carry this flag (defaults to false), restoring the real state. + */ + var isDisabledInternally: Boolean + get() = getBooleanProperty(::isDisabledInternally.name) { false } + set(value) { + setBooleanProperty(::isDisabledInternally.name, value) + } + var type: SubscriptionType get() = getEnumProperty(::type.name) set(value) { diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationModelStoreTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationModelStoreTests.kt index d4fcee869d..644b4f4102 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationModelStoreTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationModelStoreTests.kt @@ -28,7 +28,7 @@ class OperationModelStoreTests : FunSpec({ val jsonArray = JSONArray() // 1. Create a VALID Operation with onesignalId - val validOperation = SetPropertyOperation(UUID.randomUUID().toString(), UUID.randomUUID().toString(), "property", "value") + val validOperation = SetPropertyOperation(UUID.randomUUID().toString(), UUID.randomUUID().toString(), null, "property", "value") validOperation.id = UUID.randomUUID().toString() // 2. Create a VALID operation missing onesignalId diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt index 164949612c..d1f129bc2c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt @@ -12,6 +12,7 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.MockHelper import com.onesignal.mocks.MockPreferencesService +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState import com.onesignal.user.internal.operations.LoginUserOperation import io.kotest.core.spec.style.FunSpec @@ -72,6 +73,8 @@ private class Mocks { configModelStore, Time(), getNewRecordState(configModelStore), + mockk(relaxed = true), + MockHelper.identityModelStore(), ), recordPrivateCalls = true, ) @@ -97,6 +100,8 @@ class OperationRepoTests : FunSpec({ mocks.configModelStore, Time(), getNewRecordState(mocks.configModelStore), + mockk(relaxed = true), + MockHelper.identityModelStore(), ), ) @@ -889,6 +894,147 @@ class OperationRepoTests : FunSpec({ // Verify that the grouped execution happened with both operations // We can't easily verify the exact list content with MockK, but we verified it in the execution order tracking } + + test("FAIL_UNAUTHORIZED invalidates JWT and fires handler for identified user") { + // Given + val configModelStore = + MockHelper.configModelStore { + it.useIdentityVerification = true + } + val identityModelStore = + MockHelper.identityModelStore { + it.externalId = "test-user" + } + val jwtTokenStore = mockk(relaxed = true) + every { jwtTokenStore.getJwt("test-user") } returns "valid-jwt" + + val operationModelStore = + run { + val operationStoreList = mutableListOf() + val mock = mockk() + every { mock.loadOperations() } just runs + every { mock.list() } answers { operationStoreList.toList() } + every { mock.add(any()) } answers { operationStoreList.add(firstArg()) } + every { mock.remove(any()) } answers { + val id = firstArg() + operationStoreList.removeIf { it.id == id } + } + mock + } + + val executor = mockk() + every { executor.operations } returns listOf("DUMMY_OPERATION") + coEvery { executor.execute(any()) } returns + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) andThen + ExecutionResponse(ExecutionResult.SUCCESS) + + val operationRepo = + spyk( + OperationRepo( + listOf(executor), + operationModelStore, + configModelStore, + Time(), + getNewRecordState(configModelStore), + jwtTokenStore, + identityModelStore, + ), + recordPrivateCalls = true, + ) + + var handlerCalledWith: String? = null + operationRepo.setJwtInvalidatedHandler { externalId -> + handlerCalledWith = externalId + } + + val operation = mockOperation() + every { operation.externalId } returns "test-user" + + // When + operationRepo.start() + val response = operationRepo.enqueueAndWait(operation) + + // Then – waiter is woken with false immediately on FAIL_UNAUTHORIZED + // (operation is re-queued with waiter=null for retry when a new JWT is provided) + response shouldBe false + verify { jwtTokenStore.invalidateJwt("test-user") } + handlerCalledWith shouldBe "test-user" + } + + test("FAIL_UNAUTHORIZED still re-queues when JWT invalidated handler throws") { + val configModelStore = + MockHelper.configModelStore { + it.useIdentityVerification = true + } + val identityModelStore = + MockHelper.identityModelStore { + it.externalId = "test-user" + } + val jwtTokenStore = mockk(relaxed = true) + every { jwtTokenStore.getJwt("test-user") } returns "valid-jwt" + + val operationModelStore = + run { + val operationStoreList = mutableListOf() + val mock = mockk() + every { mock.loadOperations() } just runs + every { mock.list() } answers { operationStoreList.toList() } + every { mock.add(any()) } answers { operationStoreList.add(firstArg()) } + every { mock.remove(any()) } answers { + val id = firstArg() + operationStoreList.removeIf { it.id == id } + } + mock + } + + val executor = mockk() + every { executor.operations } returns listOf("DUMMY_OPERATION") + coEvery { executor.execute(any()) } returns + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) andThen + ExecutionResponse(ExecutionResult.SUCCESS) + + val operationRepo = + OperationRepo( + listOf(executor), + operationModelStore, + configModelStore, + Time(), + getNewRecordState(configModelStore), + jwtTokenStore, + identityModelStore, + ) + + operationRepo.setJwtInvalidatedHandler { throw IllegalStateException("app callback failed") } + + val operation = mockOperation() + every { operation.externalId } returns "test-user" + + operationRepo.start() + val response = operationRepo.enqueueAndWait(operation) + + // Waiter is woken with false immediately; operation re-queued with waiter=null + response shouldBe false + verify { jwtTokenStore.invalidateJwt("test-user") } + // The re-queued op (waiter=null) retries asynchronously; wait for it to complete + delay(3000) + coVerify(exactly = 2) { executor.execute(any()) } + } + + test("FAIL_UNAUTHORIZED drops operations for anonymous user") { + // Given + val mocks = Mocks() + coEvery { mocks.executor.execute(any()) } returns ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED) + + val operation = mockOperation() + + // When + mocks.operationRepo.start() + val response = mocks.operationRepo.enqueueAndWait(operation) + + // Then + response shouldBe false + verify { mocks.operationModelStore.remove(any()) } + } }) { companion object { private fun mockOperation( @@ -913,6 +1059,9 @@ class OperationRepoTests : FunSpec({ every { operation.modifyComparisonKey } returns modifyComparisonKey every { operation.translateIds(any()) } just runs every { operation.applyToRecordId } returns applyToRecordId + every { operation.requiresJwt } returns true + every { operation.externalId } returns null + every { operation.externalId = any() } just runs return operation } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LoginHelperTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LoginHelperTests.kt index a501e73bcf..70d9cbd3b7 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LoginHelperTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LoginHelperTests.kt @@ -6,10 +6,12 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.identity.IdentityModel +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.operations.LoginUserOperation import com.onesignal.user.internal.properties.PropertiesModel import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -18,15 +20,7 @@ import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.runBlocking -/** - * Unit tests for the LoginHelper class - * - * These tests focus on the pure business logic of user login operations, - * complementing the integration tests in SDKInitTests.kt which test - * end-to-end SDK initialization and login behavior. - */ class LoginHelperTests : FunSpec({ - // Test constants - using consistent naming with SDKInitTests val appId = "appId" val currentExternalId = "current-user" val newExternalId = "new-user" @@ -37,8 +31,26 @@ class LoginHelperTests : FunSpec({ Logging.logLevel = LogLevel.NONE } - test("login with same external id returns early without creating user") { - // Given + fun createLoginHelper( + identityModelStore: com.onesignal.user.internal.identity.IdentityModelStore, + userSwitcher: UserSwitcher = mockk(relaxed = true), + operationRepo: IOperationRepo = mockk(relaxed = true), + configModel: ConfigModel = mockk().also { + every { it.appId } returns appId + every { it.useIdentityVerification } returns null + }, + jwtTokenStore: JwtTokenStore = mockk(relaxed = true), + lock: Any = Any(), + ) = LoginHelper( + identityModelStore = identityModelStore, + userSwitcher = userSwitcher, + operationRepo = operationRepo, + configModel = configModel, + jwtTokenStore = jwtTokenStore, + lock = lock, + ) + + test("switchUser with same external id returns null without creating user") { val mockIdentityModelStore = MockHelper.identityModelStore { model -> model.externalId = currentExternalId @@ -46,31 +58,22 @@ class LoginHelperTests : FunSpec({ } val mockUserSwitcher = mockk(relaxed = true) val mockOperationRepo = mockk(relaxed = true) - val mockConfigModel = mockk() - every { mockConfigModel.appId } returns appId - val loginLock = Any() val loginHelper = - LoginHelper( + createLoginHelper( identityModelStore = mockIdentityModelStore, userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, - configModel = mockConfigModel, - lock = loginLock, ) - // When - runBlocking { - loginHelper.login(currentExternalId) - } + val context = loginHelper.switchUser(currentExternalId) - // Then - should return early without any operations + context shouldBe null verify(exactly = 0) { mockUserSwitcher.createAndSwitchToNewUser(suppressBackendOperation = any(), modify = any()) } coVerify(exactly = 0) { mockOperationRepo.enqueueAndWait(any()) } } - test("login with different external id creates and switches to new user") { - // Given + test("switchUser with different external id creates and switches to new user") { val mockIdentityModelStore = MockHelper.identityModelStore { model -> model.externalId = currentExternalId @@ -85,9 +88,6 @@ class LoginHelperTests : FunSpec({ val mockUserSwitcher = mockk() val mockOperationRepo = mockk() - val mockConfigModel = mockk() - every { mockConfigModel.appId } returns appId - val loginLock = Any() val userSwitcherSlot = slot<(IdentityModel, PropertiesModel) -> Unit>() every { @@ -103,24 +103,65 @@ class LoginHelperTests : FunSpec({ coEvery { mockOperationRepo.enqueueAndWait(any()) } returns true val loginHelper = - LoginHelper( + createLoginHelper( identityModelStore = mockIdentityModelStore, userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, - configModel = mockConfigModel, - lock = loginLock, ) - // When - runBlocking { - loginHelper.login(newExternalId) - } + val context = loginHelper.switchUser(newExternalId) + + context shouldNotBe null + context!!.appId shouldBe appId + context.newIdentityOneSignalId shouldBe newOneSignalId + context.externalId shouldBe newExternalId - // Then - should switch users and enqueue login operation verify(exactly = 1) { mockUserSwitcher.createAndSwitchToNewUser(suppressBackendOperation = any(), modify = any()) } userSwitcherSlot.captured(newIdentityModel, PropertiesModel()) newIdentityModel.externalId shouldBe newExternalId + } + + test("enqueueLogin enqueues login operation and returns") { + val mockIdentityModelStore = + MockHelper.identityModelStore { model -> + model.externalId = currentExternalId + model.onesignalId = currentOneSignalId + } + + val newIdentityModel = + IdentityModel().apply { + externalId = newExternalId + onesignalId = newOneSignalId + } + + val mockUserSwitcher = mockk() + val mockOperationRepo = mockk() + + val userSwitcherSlot = slot<(IdentityModel, PropertiesModel) -> Unit>() + every { + mockUserSwitcher.createAndSwitchToNewUser( + suppressBackendOperation = any(), + modify = capture(userSwitcherSlot), + ) + } answers { + userSwitcherSlot.captured(newIdentityModel, PropertiesModel()) + every { mockIdentityModelStore.model } returns newIdentityModel + } + + coEvery { mockOperationRepo.enqueueAndWait(any()) } returns true + + val loginHelper = + createLoginHelper( + identityModelStore = mockIdentityModelStore, + userSwitcher = mockUserSwitcher, + operationRepo = mockOperationRepo, + ) + + val context = loginHelper.switchUser(newExternalId)!! + runBlocking { + loginHelper.enqueueLogin(context) + } coVerify(exactly = 1) { mockOperationRepo.enqueueAndWait( @@ -128,14 +169,13 @@ class LoginHelperTests : FunSpec({ operation.appId shouldBe appId operation.onesignalId shouldBe newOneSignalId operation.externalId shouldBe newExternalId - operation.existingOnesignalId shouldBe null // Current user already has external ID, so no existing OneSignal ID + operation.existingOnesignalId shouldBe null }, ) } } - test("login with null current external id provides existing onesignal id for conversion") { - // Given - anonymous user (no external ID) + test("switchUser with null current external id provides existing onesignal id for conversion") { val mockIdentityModelStore = MockHelper.identityModelStore { model -> model.externalId = null @@ -152,7 +192,7 @@ class LoginHelperTests : FunSpec({ val mockOperationRepo = mockk() val mockConfigModel = mockk() every { mockConfigModel.appId } returns appId - val loginLock = Any() + every { mockConfigModel.useIdentityVerification } returns false val userSwitcherSlot = slot<(IdentityModel, PropertiesModel) -> Unit>() every { @@ -168,34 +208,31 @@ class LoginHelperTests : FunSpec({ coEvery { mockOperationRepo.enqueueAndWait(any()) } returns true val loginHelper = - LoginHelper( + createLoginHelper( identityModelStore = mockIdentityModelStore, userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, configModel = mockConfigModel, - lock = loginLock, ) - // When + val context = loginHelper.switchUser(newExternalId)!! runBlocking { - loginHelper.login(newExternalId) + loginHelper.enqueueLogin(context) } - // Then - should provide existing OneSignal ID for anonymous user conversion coVerify(exactly = 1) { mockOperationRepo.enqueueAndWait( withArg { operation -> operation.appId shouldBe appId operation.onesignalId shouldBe newOneSignalId operation.externalId shouldBe newExternalId - operation.existingOnesignalId shouldBe currentOneSignalId // For conversion + operation.existingOnesignalId shouldBe currentOneSignalId }, ) } } - test("login logs error when operation fails") { - // Given + test("enqueueLogin logs warning when operation fails") { val mockIdentityModelStore = MockHelper.identityModelStore { model -> model.externalId = currentExternalId @@ -210,9 +247,6 @@ class LoginHelperTests : FunSpec({ val mockUserSwitcher = mockk() val mockOperationRepo = mockk() - val mockConfigModel = mockk() - every { mockConfigModel.appId } returns appId - val loginLock = Any() val userSwitcherSlot = slot<(IdentityModel, PropertiesModel) -> Unit>() every { @@ -225,24 +259,20 @@ class LoginHelperTests : FunSpec({ every { mockIdentityModelStore.model } returns newIdentityModel } - // Mock operation failure coEvery { mockOperationRepo.enqueueAndWait(any()) } returns false val loginHelper = - LoginHelper( + createLoginHelper( identityModelStore = mockIdentityModelStore, userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, - configModel = mockConfigModel, - lock = loginLock, ) - // When + val context = loginHelper.switchUser(newExternalId)!! runBlocking { - loginHelper.login(newExternalId) + loginHelper.enqueueLogin(context) } - // Then - should still switch users but operation fails verify(exactly = 1) { mockUserSwitcher.createAndSwitchToNewUser(suppressBackendOperation = any(), modify = any()) } coVerify(exactly = 1) { mockOperationRepo.enqueueAndWait(any()) } } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LogoutHelperTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LogoutHelperTests.kt index 4921ed6bb6..68f3ffe281 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LogoutHelperTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/LogoutHelperTests.kt @@ -6,6 +6,8 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.operations.LoginUserOperation +import com.onesignal.user.internal.subscriptions.SubscriptionModel +import com.onesignal.user.internal.subscriptions.SubscriptionModelStore import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.every @@ -41,6 +43,7 @@ class LogoutHelperTests : FunSpec({ val mockOperationRepo = mockk(relaxed = true) val mockConfigModel = mockk() every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns false val logoutLock = Any() val logoutHelper = @@ -49,6 +52,7 @@ class LogoutHelperTests : FunSpec({ userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, configModel = mockConfigModel, + subscriptionModelStore = mockk(relaxed = true), lock = logoutLock, ) @@ -71,6 +75,7 @@ class LogoutHelperTests : FunSpec({ val mockOperationRepo = mockk(relaxed = true) val mockConfigModel = mockk() every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns false val logoutLock = Any() val logoutHelper = @@ -79,6 +84,7 @@ class LogoutHelperTests : FunSpec({ userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, configModel = mockConfigModel, + subscriptionModelStore = mockk(relaxed = true), lock = logoutLock, ) @@ -110,6 +116,7 @@ class LogoutHelperTests : FunSpec({ val mockOperationRepo = mockk(relaxed = true) val mockConfigModel = mockk() every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns false val logoutLock = Any() val logoutHelper = @@ -118,6 +125,7 @@ class LogoutHelperTests : FunSpec({ userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, configModel = mockConfigModel, + subscriptionModelStore = mockk(relaxed = true), lock = logoutLock, ) @@ -142,6 +150,7 @@ class LogoutHelperTests : FunSpec({ val mockOperationRepo = mockk(relaxed = true) val mockConfigModel = mockk() every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns false val logoutLock = Any() val logoutHelper = @@ -150,6 +159,7 @@ class LogoutHelperTests : FunSpec({ userSwitcher = mockUserSwitcher, operationRepo = mockOperationRepo, configModel = mockConfigModel, + subscriptionModelStore = mockk(relaxed = true), lock = logoutLock, ) @@ -168,4 +178,85 @@ class LogoutHelperTests : FunSpec({ verify(atLeast = 1) { mockUserSwitcher.createAndSwitchToNewUser() } verify(atLeast = 1) { mockOperationRepo.enqueue(any()) } } + + test("logout with IV=true disables push and suppresses backend operation") { + // Given - identified user with IV enabled + val pushSubId = "push-sub-id" + val mockSubscriptionModel = mockk(relaxed = true) + val mockIdentityModelStore = + MockHelper.identityModelStore { model -> + model.externalId = externalId + model.onesignalId = onesignalId + } + val mockUserSwitcher = mockk(relaxed = true) + val mockOperationRepo = mockk(relaxed = true) + val mockConfigModel = mockk() + every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns true + every { mockConfigModel.pushSubscriptionId } returns pushSubId + val mockSubscriptionModelStore = mockk(relaxed = true) + every { mockSubscriptionModelStore.get(pushSubId) } returns mockSubscriptionModel + + val logoutHelper = + LogoutHelper( + identityModelStore = mockIdentityModelStore, + userSwitcher = mockUserSwitcher, + operationRepo = mockOperationRepo, + configModel = mockConfigModel, + subscriptionModelStore = mockSubscriptionModelStore, + lock = Any(), + ) + + // When + logoutHelper.logout() + + // Then + verify { mockSubscriptionModel.isDisabledInternally = true } + verify { mockUserSwitcher.createAndSwitchToNewUser(suppressBackendOperation = true) } + verify(exactly = 0) { mockOperationRepo.enqueue(any()) } + } + + test("logout with IV=null (pre-HYDRATE) disables push and enqueues anonymous user") { + // Given - identified user, IV state unknown + val pushSubId = "push-sub-id" + val mockSubscriptionModel = mockk(relaxed = true) + val mockIdentityModelStore = + MockHelper.identityModelStore { model -> + model.externalId = externalId + model.onesignalId = onesignalId + } + val mockUserSwitcher = mockk(relaxed = true) + val mockOperationRepo = mockk(relaxed = true) + val mockConfigModel = mockk() + every { mockConfigModel.appId } returns appId + every { mockConfigModel.useIdentityVerification } returns null + every { mockConfigModel.pushSubscriptionId } returns pushSubId + val mockSubscriptionModelStore = mockk(relaxed = true) + every { mockSubscriptionModelStore.get(pushSubId) } returns mockSubscriptionModel + + val logoutHelper = + LogoutHelper( + identityModelStore = mockIdentityModelStore, + userSwitcher = mockUserSwitcher, + operationRepo = mockOperationRepo, + configModel = mockConfigModel, + subscriptionModelStore = mockSubscriptionModelStore, + lock = Any(), + ) + + // When + logoutHelper.logout() + + // Then - push disabled, no suppression, anonymous LoginUserOperation enqueued + verify { mockSubscriptionModel.isDisabledInternally = true } + verify { mockUserSwitcher.createAndSwitchToNewUser() } + verify { + mockOperationRepo.enqueue( + withArg { operation -> + operation.appId shouldBe appId + operation.externalId shouldBe null + }, + ) + } + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/migrations/RecoverFromDroppedLoginBugTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/migrations/RecoverFromDroppedLoginBugTests.kt index 554c09ac96..0caf8f2f49 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/migrations/RecoverFromDroppedLoginBugTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/migrations/RecoverFromDroppedLoginBugTests.kt @@ -6,6 +6,7 @@ import com.onesignal.core.internal.time.impl.Time import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks import com.onesignal.user.internal.operations.LoginUserOperation import io.kotest.core.spec.style.FunSpec @@ -38,6 +39,8 @@ private class Mocks { configModelStore, Time(), ExecutorMocks.getNewRecordState(configModelStore), + mockk(relaxed = true), + MockHelper.identityModelStore(), ), ) 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..5c6d8d6c60 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 @@ -9,6 +9,7 @@ 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.identity.JwtTokenStore import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.equals.shouldBeEqual @@ -36,7 +37,7 @@ class CustomEventOperationExecutorTests : FunSpec({ val properties = JSONObject().put("key", "value").toString() val customEventOperationExecutor = - CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService) + CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService, mockk(relaxed = true)) val operations = listOf(TrackCustomEventOperation("appId", "onesignalId", null, 1, "event-name", properties)) // When 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 34d0681c48..8ad425895c 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 @@ -10,6 +10,7 @@ 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.identity.JwtTokenStore 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,8 +40,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(SetAliasOperation("appId", "onesignalId", "aliasKey1", "aliasValue1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When val response = identityOperationExecutor.execute(operations) @@ -69,8 +70,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(SetAliasOperation("appId", "onesignalId", "aliasKey1", "aliasValue1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -90,8 +91,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(SetAliasOperation("appId", "onesignalId", "aliasKey1", "aliasValue1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -111,8 +112,8 @@ class IdentityOperationExecutorTests : FunSpec({ every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } returns null val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(SetAliasOperation("appId", "onesignalId", "aliasKey1", "aliasValue1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -134,8 +135,8 @@ 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) - val operations = listOf(SetAliasOperation("appId", "onesignalId", "aliasKey1", "aliasValue1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, mockConfigModelStore, mockk(relaxed = true)) + val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -160,8 +161,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(DeleteAliasOperation("appId", "onesignalId", "aliasKey1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When val response = identityOperationExecutor.execute(operations) @@ -183,8 +184,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(DeleteAliasOperation("appId", "onesignalId", "aliasKey1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -203,8 +204,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(DeleteAliasOperation("appId", "onesignalId", "aliasKey1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -225,8 +226,8 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) - val operations = listOf(DeleteAliasOperation("appId", "onesignalId", "aliasKey1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), MockHelper.configModelStore(), mockk(relaxed = true)) + val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -250,8 +251,8 @@ 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) - val operations = listOf(DeleteAliasOperation("appId", "onesignalId", "aliasKey1")) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, mockConfigModelStore, mockk(relaxed = true)) + val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When val response = identityOperationExecutor.execute(operations) 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 d80dc5531a..df9b566f2f 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,7 @@ 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.identity.JwtTokenStore 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 @@ -43,6 +44,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, "subscriptionId1", SubscriptionType.PUSH, true, @@ -53,7 +55,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("login anonymous user successfully creates user") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -76,6 +78,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -94,6 +97,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mapOf(), any(), any(), + any(), ) } } @@ -101,7 +105,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("login anonymous user fails with retry when network condition exists") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } throws BackendException(408, "TIMEOUT", retryAfterSeconds = 10) + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } throws BackendException(408, "TIMEOUT", retryAfterSeconds = 10) val mockIdentityOperationExecutor = mockk() @@ -120,6 +124,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -133,13 +138,13 @@ class LoginUserOperationExecutorTests : FunSpec({ // Then response.result shouldBe ExecutionResult.FAIL_RETRY response.retryAfterSeconds shouldBe 10 - coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } + coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any(), any()) } } test("login anonymous user fails with no retry when backend error condition exists") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } throws BackendException(404, "NOT FOUND") + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } throws BackendException(404, "NOT FOUND") val mockIdentityOperationExecutor = mockk() @@ -148,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(), mockk(relaxed = true)) val operations = listOf( LoginUserOperation(appId, localOneSignalId, null, null), @@ -160,13 +165,13 @@ class LoginUserOperationExecutorTests : FunSpec({ // Then response.result shouldBe ExecutionResult.FAIL_PAUSE_OPREPO - coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } + coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any(), any()) } } test("login identified user without association successfully creates user") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) val mockIdentityOperationExecutor = mockk() @@ -176,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(), mockk(relaxed = true)) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null)) // When @@ -186,7 +191,7 @@ class LoginUserOperationExecutorTests : FunSpec({ response.result shouldBe ExecutionResult.SUCCESS coVerify( exactly = 1, - ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any()) } + ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any(), any()) } } // If the User is identified then the backend may have found an existing User, if so @@ -194,7 +199,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("login identified user returns result with RefreshUser") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) val mockIdentityOperationExecutor = mockk() @@ -214,6 +219,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null)) @@ -242,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(), mockk(relaxed = true)) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -267,7 +273,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("login identified user with association fails with retry when association fails with retry") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) val mockIdentityOperationExecutor = mockk() @@ -278,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(), mockk(relaxed = true)) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -303,7 +309,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("login identified user with association successfully creates user when association fails with no retry") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) val mockIdentityOperationExecutor = mockk() @@ -314,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(), mockk(relaxed = true)) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -336,13 +342,13 @@ class LoginUserOperationExecutorTests : FunSpec({ } coVerify( exactly = 1, - ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any()) } + ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any(), any()) } } test("login identified user with association fails with retry when association fails with no retry and network condition exists") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } throws BackendException(408, "TIMEOUT") + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } throws BackendException(408, "TIMEOUT") val mockIdentityOperationExecutor = mockk() coEvery { mockIdentityOperationExecutor.execute(any()) } returns ExecutionResponse(ExecutionResult.FAIL_NORETRY) @@ -352,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(), mockk(relaxed = true)) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -374,13 +380,13 @@ class LoginUserOperationExecutorTests : FunSpec({ } coVerify( exactly = 1, - ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any()) } + ) { mockUserBackendService.createUser(appId, mapOf(IdentityConstants.EXTERNAL_ID to "externalId"), any(), any(), any()) } } test("creating user will merge operations into one backend call") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -403,6 +409,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -410,6 +417,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, "subscriptionId1", SubscriptionType.PUSH, true, @@ -419,6 +427,7 @@ class LoginUserOperationExecutorTests : FunSpec({ UpdateSubscriptionOperation( appId, localOneSignalId, + null, "subscriptionId1", SubscriptionType.PUSH, true, @@ -428,13 +437,14 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, "subscriptionId2", SubscriptionType.EMAIL, true, "name@company.com", SubscriptionStatus.SUBSCRIBED, ), - DeleteSubscriptionOperation(appId, localOneSignalId, "subscriptionId2"), + DeleteSubscriptionOperation(appId, localOneSignalId, null, "subscriptionId2"), ) // When @@ -459,6 +469,7 @@ class LoginUserOperationExecutorTests : FunSpec({ SubscriptionStatus.fromInt(subscription.notificationTypes!!) shouldBe SubscriptionStatus.SUBSCRIBED }, any(), + any(), ) } } @@ -466,7 +477,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("creating user will hydrate when the user hasn't changed") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -504,6 +515,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -511,6 +523,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId1, SubscriptionType.PUSH, true, @@ -520,6 +533,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId2, SubscriptionType.EMAIL, true, @@ -545,6 +559,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mapOf(), any(), any(), + any(), ) } } @@ -552,7 +567,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("creating user will not hydrate when the user has changed") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -590,6 +605,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -597,6 +613,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId1, SubscriptionType.PUSH, true, @@ -606,6 +623,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId2, SubscriptionType.EMAIL, true, @@ -631,6 +649,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mapOf(), any(), any(), + any(), ) } } @@ -638,7 +657,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("creating user will provide local to remote translations") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -662,6 +681,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) val operations = listOf( @@ -669,6 +689,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId1, SubscriptionType.PUSH, true, @@ -678,6 +699,7 @@ class LoginUserOperationExecutorTests : FunSpec({ CreateSubscriptionOperation( appId, localOneSignalId, + null, localSubscriptionId2, SubscriptionType.EMAIL, true, @@ -698,6 +720,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mapOf(), any(), any(), + any(), ) } } @@ -705,7 +728,7 @@ class LoginUserOperationExecutorTests : FunSpec({ test("ensure anonymous login with no other operations will fail with FAIL_NORETRY") { // Given val mockUserBackendService = mockk() - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf()) val mockIdentityOperationExecutor = mockk() @@ -725,6 +748,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) // anonymous Login request val operations = listOf(LoginUserOperation(appId, localOneSignalId, null, null)) @@ -737,14 +761,14 @@ class LoginUserOperationExecutorTests : FunSpec({ // ensure user is not created by the bad request coVerify( exactly = 0, - ) { mockUserBackendService.createUser(appId, any(), any(), any()) } + ) { mockUserBackendService.createUser(appId, any(), any(), any(), any()) } } test("create user maps subscriptions when backend order is different (match by id/token)") { // Given val mockUserBackendService = mockk() // backend returns EMAIL first (with token), then PUSH — out of order - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -771,14 +795,15 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + mockk(relaxed = true), ) // send PUSH then EMAIL (local IDs 1,2) — order differs from backend response val ops = listOf( LoginUserOperation(appId, localOneSignalId, null, null), - CreateSubscriptionOperation(appId, localOneSignalId, localSubscriptionId1, SubscriptionType.PUSH, true, "pushToken2", SubscriptionStatus.SUBSCRIBED), - CreateSubscriptionOperation(appId, localOneSignalId, localSubscriptionId2, SubscriptionType.EMAIL, true, "name@company.com", SubscriptionStatus.SUBSCRIBED), + CreateSubscriptionOperation(appId, localOneSignalId, null, localSubscriptionId1, SubscriptionType.PUSH, true, "pushToken2", SubscriptionStatus.SUBSCRIBED), + CreateSubscriptionOperation(appId, localOneSignalId, null, localSubscriptionId2, SubscriptionType.EMAIL, true, "name@company.com", SubscriptionStatus.SUBSCRIBED), ) // When @@ -795,14 +820,14 @@ class LoginUserOperationExecutorTests : FunSpec({ // email localSubscriptionId2 to remoteSubscriptionId2, ) - coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } + coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any(), any()) } } test("create user maps push subscription by type when id and token don't match (case for deleted push sub)") { // Given val mockUserBackendService = mockk() // simulate server-side push sub recreated with new ID and no token; must match by type - coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), @@ -835,12 +860,13 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, configModelStore, MockHelper.languageContext(), + mockk(relaxed = true), ) val ops = listOf( LoginUserOperation(appId, localOneSignalId, null, null), - CreateSubscriptionOperation(appId, localOneSignalId, localSubscriptionId1, SubscriptionType.PUSH, true, "pushToken1", SubscriptionStatus.SUBSCRIBED), + CreateSubscriptionOperation(appId, localOneSignalId, null, localSubscriptionId1, SubscriptionType.PUSH, true, "pushToken1", SubscriptionStatus.SUBSCRIBED), ) // When @@ -857,6 +883,6 @@ class LoginUserOperationExecutorTests : FunSpec({ localPushModel.id shouldBe remoteSubscriptionId1 // pushSubscriptionId should be updated from local to remote id configModelStore.model.pushSubscriptionId shouldBe remoteSubscriptionId1 - coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } + coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any(), 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 2689d761af..12f71ff579 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,7 @@ 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.identity.JwtTokenStore 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,9 +108,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ mockConfigModelStore, mockBuildUserService, getNewRecordState(), + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) try { // When @@ -191,9 +193,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) // When val response = refreshUserOperationExecutor.execute(operations) @@ -230,9 +233,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) // When val response = refreshUserOperationExecutor.execute(operations) @@ -265,9 +269,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) // When val response = refreshUserOperationExecutor.execute(operations) @@ -300,9 +305,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) // When val response = refreshUserOperationExecutor.execute(operations) @@ -337,9 +343,10 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, newRecordState, + mockk(relaxed = true), ) - val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) // When val response = refreshUserOperationExecutor.execute(operations) 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 4ae3053247..7b5a8a5b14 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,7 @@ 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.identity.JwtTokenStore 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 +69,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -75,6 +77,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -128,6 +131,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -135,6 +139,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -178,6 +183,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -185,6 +191,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -233,6 +240,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -240,6 +248,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -288,6 +297,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -295,6 +305,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -331,6 +342,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -338,13 +350,14 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, "pushToken", SubscriptionStatus.SUBSCRIBED, ), - DeleteSubscriptionOperation(appId, remoteOneSignalId, localSubscriptionId), + DeleteSubscriptionOperation(appId, remoteOneSignalId, null, localSubscriptionId), ) // When @@ -377,6 +390,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -384,6 +398,7 @@ class SubscriptionOperationExecutorTests : CreateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -393,6 +408,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, localSubscriptionId, SubscriptionType.PUSH, true, @@ -447,6 +463,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -454,6 +471,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -463,6 +481,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -508,6 +527,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -515,6 +535,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -560,6 +581,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -567,6 +589,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -614,6 +637,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -621,6 +645,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, @@ -656,11 +681,12 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = listOf( - DeleteSubscriptionOperation(appId, remoteOneSignalId, remoteSubscriptionId), + DeleteSubscriptionOperation(appId, remoteOneSignalId, null, remoteSubscriptionId), ) // When @@ -690,11 +716,12 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = listOf( - DeleteSubscriptionOperation(appId, remoteOneSignalId, remoteSubscriptionId), + DeleteSubscriptionOperation(appId, remoteOneSignalId, null, remoteSubscriptionId), ) // When @@ -725,11 +752,12 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = listOf( - DeleteSubscriptionOperation(appId, remoteOneSignalId, remoteSubscriptionId), + DeleteSubscriptionOperation(appId, remoteOneSignalId, null, remoteSubscriptionId), ) // When @@ -760,11 +788,12 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + mockk(relaxed = true), ) val operations = listOf( - DeleteSubscriptionOperation(appId, remoteOneSignalId, remoteSubscriptionId), + DeleteSubscriptionOperation(appId, remoteOneSignalId, null, remoteSubscriptionId), ) // When @@ -801,6 +830,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + mockk(relaxed = true), ) val operations = @@ -808,6 +838,7 @@ class SubscriptionOperationExecutorTests : UpdateSubscriptionOperation( appId, remoteOneSignalId, + null, remoteSubscriptionId, SubscriptionType.PUSH, true, 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 de2e148ff8..e69b64fd3d 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,7 @@ 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.identity.JwtTokenStore 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,8 +57,10 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) - val operations = listOf(SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1")) + val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) // When val response = loginUserOperationExecutor.execute(operations) @@ -96,24 +99,26 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) val operations = listOf( - SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1-1"), - SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1-2"), - SetTagOperation(appId, remoteOneSignalId, "tagKey2", "tagValue2"), - SetTagOperation(appId, remoteOneSignalId, "tagKey3", "tagValue3"), - DeleteTagOperation(appId, remoteOneSignalId, "tagKey3"), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::language.name, "lang1"), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::language.name, "lang2"), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::timezone.name, "timezone"), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::country.name, "country"), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationLatitude.name, 123.45), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationLongitude.name, 678.90), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationType.name, 1), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationAccuracy.name, 0.15), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationBackground.name, true), - SetPropertyOperation(appId, localOneSignalId, PropertiesModel::locationTimestamp.name, 1111L), + SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1-1"), + SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1-2"), + SetTagOperation(appId, remoteOneSignalId, null, "tagKey2", "tagValue2"), + SetTagOperation(appId, remoteOneSignalId, null, "tagKey3", "tagValue3"), + DeleteTagOperation(appId, remoteOneSignalId, null, "tagKey3"), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::language.name, "lang1"), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::language.name, "lang2"), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::timezone.name, "timezone"), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::country.name, "country"), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationLatitude.name, 123.45), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationLongitude.name, 678.90), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationType.name, 1), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationAccuracy.name, 0.15), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationBackground.name, true), + SetPropertyOperation(appId, localOneSignalId, null, PropertiesModel::locationTimestamp.name, 1111L), ) // When @@ -158,10 +163,12 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) val operations = listOf( - TrackSessionEndOperation(appId, remoteOneSignalId, 1111), + TrackSessionEndOperation(appId, remoteOneSignalId, null, 1111), ) // When @@ -203,13 +210,16 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) val operations = listOf( - TrackSessionEndOperation(appId, remoteOneSignalId, 1111), + TrackSessionEndOperation(appId, remoteOneSignalId, null, 1111), TrackPurchaseOperation( appId, remoteOneSignalId, + null, false, BigDecimal(2222), listOf( @@ -217,7 +227,7 @@ class UpdateUserOperationExecutorTests : PurchaseInfo("sku2", "iso2", BigDecimal(1222)), ), ), - TrackSessionEndOperation(appId, remoteOneSignalId, 3333), + TrackSessionEndOperation(appId, remoteOneSignalId, null, 3333), ) // When @@ -268,12 +278,14 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) val operations = listOf( - TrackSessionEndOperation(appId, remoteOneSignalId, 1111), - SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1"), - TrackSessionEndOperation(appId, remoteOneSignalId, 3333), + TrackSessionEndOperation(appId, remoteOneSignalId, null, 1111), + SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1"), + TrackSessionEndOperation(appId, remoteOneSignalId, null, 3333), ) // When @@ -316,8 +328,10 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) - val operations = listOf(SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1")) + val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) // When val response = loginUserOperationExecutor.execute(operations) @@ -349,8 +363,10 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) - val operations = listOf(SetTagOperation(appId, remoteOneSignalId, "tagKey1", "tagValue1")) + val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) // When val response = loginUserOperationExecutor.execute(operations) @@ -379,11 +395,13 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + MockHelper.configModelStore(), + mockk(relaxed = true), ) val operations = listOf( - TrackSessionStartOperation(appId, onesignalId = remoteOneSignalId), + TrackSessionStartOperation(appId, onesignalId = remoteOneSignalId, externalId = null), ) // When diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt index b4994a0aeb..a5f4c60455 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt @@ -48,6 +48,7 @@ import com.onesignal.user.IUserManager import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.subscriptions.ISubscriptionChangedHandler import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionModel @@ -76,6 +77,7 @@ internal class InAppMessagesManager( private val _languageContext: ILanguageContext, private val _time: ITime, private val _consistencyManager: IConsistencyManager, + private val _jwtTokenStore: JwtTokenStore, ) : IInAppMessagesManager, IStartableService, ISubscriptionChangedHandler, @@ -299,6 +301,21 @@ internal class InAppMessagesManager( return } + val externalId = _identityModelStore.model.externalId + // Capture JWT once to avoid TOCTOU: the same snapshot is used for the guard + // check and the backend call, so a concurrent invalidation can't slip between them. + val jwt = externalId?.let { _jwtTokenStore.getJwt(it) } + if (_configModelStore.model.useIdentityVerification == true) { + if (externalId == null) { + Logging.debug("InAppMessagesManager.fetchMessages: Skipping IAM fetch for anonymous user while identity verification is enabled.") + return + } + if (jwt == null) { + Logging.debug("InAppMessagesManager.fetchMessages: Skipping IAM fetch while JWT is invalidated for user: $externalId") + return + } + } + fetchIAMMutex.withLock { val now = _time.currentTimeMillis if (lastTimeFetchedIAMs != null && (now - lastTimeFetchedIAMs!!) < _configModelStore.model.fetchIAMMinInterval) { @@ -308,9 +325,15 @@ internal class InAppMessagesManager( lastTimeFetchedIAMs = now } + val (aliasLabel, aliasValue) = + IdentityConstants.resolveAlias( + _configModelStore.model.useIdentityVerification, + externalId, + _identityModelStore.model.onesignalId, + ) // lambda so that it is updated on each potential retry val sessionDurationProvider = { _time.currentTimeMillis - _sessionService.startTime } - val newMessages = _backend.listInAppMessages(appId, subscriptionId, rywData, sessionDurationProvider) + val newMessages = _backend.listInAppMessages(appId, aliasLabel, aliasValue, subscriptionId, rywData, sessionDurationProvider, jwt) if (newMessages != null) { this.messages = newMessages as MutableList diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt index 6755b6eb5a..7044d6db3b 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt @@ -21,9 +21,12 @@ internal interface IInAppBackendService { */ suspend fun listInAppMessages( appId: String, + aliasLabel: String, + aliasValue: String, subscriptionId: String, rywData: RywData, sessionDurationProvider: () -> Long, + jwt: String? = null, ): List? /** diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt index 9bbd738d55..77a77b5f5f 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt @@ -26,15 +26,18 @@ internal class InAppBackendService( override suspend fun listInAppMessages( appId: String, + aliasLabel: String, + aliasValue: String, subscriptionId: String, rywData: RywData, sessionDurationProvider: () -> Long, + jwt: String?, ): List? { val rywDelay = rywData.rywDelay ?: DEFAULT_RYW_DELAY_MS delay(rywDelay) // Delay by the specified amount - val baseUrl = "apps/$appId/subscriptions/$subscriptionId/iams" - return attemptFetchWithRetries(baseUrl, rywData, sessionDurationProvider) + val baseUrl = "apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions/$subscriptionId/iams" + return attemptFetchWithRetries(baseUrl, rywData, sessionDurationProvider, jwt) } override suspend fun getIAMData( @@ -209,6 +212,7 @@ internal class InAppBackendService( baseUrl: String, rywData: RywData, sessionDurationProvider: () -> Long, + jwt: String? = null, ): List? { var attempts = 0 var retryLimit: Int = 0 // retry limit is remote defined & set dynamically below @@ -220,6 +224,7 @@ internal class InAppBackendService( rywToken = rywData.rywToken, sessionDuration = sessionDurationProvider(), retryCount = retryCount, + jwt = jwt, ) val response = _httpClient.get(baseUrl, values) @@ -244,18 +249,20 @@ internal class InAppBackendService( } while (attempts <= retryLimit) // Final attempt without the RYW token if retries fail - return fetchInAppMessagesWithoutRywToken(baseUrl, sessionDurationProvider) + return fetchInAppMessagesWithoutRywToken(baseUrl, sessionDurationProvider, jwt) } private suspend fun fetchInAppMessagesWithoutRywToken( url: String, sessionDurationProvider: () -> Long, + jwt: String? = null, ): List? { val response = _httpClient.get( url, OptionalHeaders( sessionDuration = sessionDurationProvider(), + jwt = jwt, ), ) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt index 418cce53cc..60a75847bd 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt @@ -31,6 +31,7 @@ import com.onesignal.session.internal.influence.IInfluenceManager import com.onesignal.session.internal.outcomes.IOutcomeEventsController import com.onesignal.session.internal.session.ISessionService import com.onesignal.user.IUserManager +import com.onesignal.user.internal.identity.JwtTokenStore import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionModel import com.onesignal.user.subscriptions.IPushSubscription @@ -77,7 +78,7 @@ private class Mocks { val inAppLifecycleService = mockk(relaxed = true) val languageContext = MockHelper.languageContext() val time = MockHelper.time(1000) - val inAppMessageLifecycleListener = spyk() + val inAppMessageLifecycleListener = mockk(relaxed = true) val inAppMessageClickListener = spyk() val rywData = RywData("token", 100L) @@ -89,6 +90,8 @@ private class Mocks { coEvery { getRywDataFromAwaitableCondition(any()) } returns rywDeferred } + val jwtTokenStore = mockk(relaxed = true) + val subscriptionManager = mockk(relaxed = true) { every { subscriptions } returns mockk { every { push } returns pushSubscription @@ -187,6 +190,7 @@ private class Mocks { languageContext, time, consistencyManager, + jwtTokenStore, ) } @@ -455,7 +459,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns null + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns null val args = ModelChangedArgs( ConfigModel(), ConfigModel::appId.name, @@ -470,7 +474,7 @@ class InAppMessagesManagerTests : FunSpec({ // Then // Should trigger fetchMessagesWhenConditionIsMet - coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onModelUpdated does nothing when non-appId property changes") { @@ -488,7 +492,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onModelReplaced fetches messages") { @@ -497,7 +501,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns null + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns null // When mocks.inAppMessagesManager.onModelReplaced(ConfigModel(), "tag") @@ -505,7 +509,7 @@ class InAppMessagesManagerTests : FunSpec({ // Then coVerify { - mocks.backend.listInAppMessages(any(), any(), any(), any()) + mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } } @@ -525,7 +529,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns null + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns null // When mocks.inAppMessagesManager.onSubscriptionChanged(mocks.pushSubscription, args) @@ -533,7 +537,7 @@ class InAppMessagesManagerTests : FunSpec({ // Then coVerify { - mocks.backend.listInAppMessages(any(), any(), any(), any()) + mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } @@ -555,7 +559,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onSubscriptionChanged does nothing when id path does not match") { @@ -575,7 +579,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onSubscriptionAdded does not fetch") { @@ -587,7 +591,7 @@ class InAppMessagesManagerTests : FunSpec({ iamManager.onSubscriptionAdded(mockSubscription) // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onSubscriptionRemoved does not fetch") { @@ -599,7 +603,7 @@ class InAppMessagesManagerTests : FunSpec({ iamManager.onSubscriptionRemoved(mockSubscription) // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } } @@ -622,7 +626,7 @@ class InAppMessagesManagerTests : FunSpec({ } returns mockDeferred every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns null + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns null // When mocks.inAppMessagesManager.start() @@ -633,7 +637,7 @@ class InAppMessagesManagerTests : FunSpec({ // Verify messages were reset and backend was called message1.isDisplayedInSession shouldBe false message2.isDisplayedInSession shouldBe false - coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("onSessionActive does nothing") { @@ -772,7 +776,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) // Fetch messages first mocks.inAppMessagesManager.onSessionStarted() @@ -792,7 +796,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) // Fetch messages first mocks.inAppMessagesManager.onSessionStarted() @@ -943,7 +947,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("fetchMessagesWhenConditionIsMet returns early when subscriptionId is empty") { @@ -957,7 +961,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("fetchMessagesWhenConditionIsMet returns early when subscriptionId is local ID") { @@ -971,7 +975,7 @@ class InAppMessagesManagerTests : FunSpec({ awaitIO() // Then - coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify(exactly = 0) { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } } test("fetchMessagesWhenConditionIsMet evaluates messages when new messages are returned") { @@ -981,14 +985,14 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" every { mocks.triggerController.evaluateMessageTriggers(any()) } returns false - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) // When mocks.inAppMessagesManager.onSessionStarted() awaitIO() // Then - coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any()) } + coVerify { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } verify { mocks.triggerController.evaluateMessageTriggers(any()) } } } @@ -1028,7 +1032,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.inAppStateService.inAppMessageIdShowing } returns null every { mocks.inAppStateService.paused } returns true coEvery { mocks.applicationService.waitUntilSystemConditionsAvailable() } returns true - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) coEvery { mocks.inAppDisplayer.displayMessage(any()) } returns true // Fetch messages first @@ -1277,7 +1281,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.triggerController.evaluateMessageTriggers(any()) } returns false // Mock backend to return both messages - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message1, message2) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message1, message2) // Start the manager to load redisplayed messages mocks.inAppMessagesManager.start() @@ -1311,8 +1315,8 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" - every { mocks.configModelStore.model.appId } returns "test-app-id" - every { mocks.configModelStore.model.fetchIAMMinInterval } returns 0L + mocks.configModelStore.model.appId = "test-app-id" + mocks.configModelStore.model.fetchIAMMinInterval = 0L every { mocks.triggerModelStore.get(any()) } returns null every { mocks.triggerModelStore.add(any()) } answers {} @@ -1324,7 +1328,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.triggerController.evaluateMessageTriggers(any()) } returns false // Mock first fetch to return the message - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) mocks.inAppMessagesManager.start() awaitIO() @@ -1344,7 +1348,7 @@ class InAppMessagesManagerTests : FunSpec({ earlySessionTriggers.contains("lateTrigger") shouldBe false // Mock second fetch to return the same message - coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(message) // Trigger second fetch mocks.inAppMessagesManager.onSessionStarted() @@ -1367,11 +1371,11 @@ class InAppMessagesManagerTests : FunSpec({ every { mockTriggerModelStore.add(any()) } answers {} coEvery { mockRepository.listInAppMessages() } returns mutableListOf() every { mockTriggerController.evaluateMessageTriggers(any()) } returns false - coEvery { mockBackend.listInAppMessages(any(), any(), any(), any()) } returns listOf(mocks.createInAppMessage()) + coEvery { mockBackend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(mocks.createInAppMessage()) every { mocks.pushSubscription.id } returns "test-sub-id" - every { mocks.configModelStore.model.appId } returns "test-app-id" - every { mocks.configModelStore.model.fetchIAMMinInterval } returns 0L + mocks.configModelStore.model.appId = "test-app-id" + mocks.configModelStore.model.fetchIAMMinInterval = 0L every { mocks.applicationService.isInForeground } returns true iamManager.start() @@ -1399,7 +1403,7 @@ class InAppMessagesManagerTests : FunSpec({ val messageAfterClear = mocks.createInAppMessage() // Mock backend for second fetch - coEvery { mockBackend.listInAppMessages(any(), any(), any(), any()) } returns listOf(messageAfterClear) + coEvery { mockBackend.listInAppMessages(any(), any(), any(), any(), any(), any(), any()) } returns listOf(messageAfterClear) // Mock that message is in redisplayed and matches the cleared triggers coEvery { mockRepository.listInAppMessages() } returns mutableListOf(messageAfterClear) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/backend/InAppBackendServiceTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/backend/InAppBackendServiceTests.kt index 72f986e0ec..d2c0eb561e 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/backend/InAppBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/backend/InAppBackendServiceTests.kt @@ -40,12 +40,12 @@ class InAppBackendServiceTests : val inAppBackendService = InAppBackendService(mockHttpClient, MockHelper.deviceService(), mockHydrator) // When - val response = inAppBackendService.listInAppMessages("appId", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) + val response = inAppBackendService.listInAppMessages("appId", "onesignal_id", "user123", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) // Then response shouldNotBe null response!!.count() shouldBe 0 - coVerify(exactly = 1) { mockHttpClient.get("apps/appId/subscriptions/subscriptionId/iams", any()) } + coVerify(exactly = 1) { mockHttpClient.get("apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", any()) } } test("listInAppMessages with 1 message returns one-lengthed array") { @@ -63,7 +63,7 @@ class InAppBackendServiceTests : val inAppBackendService = InAppBackendService(mockHttpClient, MockHelper.deviceService(), mockHydrator) // When - val response = inAppBackendService.listInAppMessages("appId", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) + val response = inAppBackendService.listInAppMessages("appId", "onesignal_id", "user123", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) // Then response shouldNotBe null @@ -84,7 +84,7 @@ class InAppBackendServiceTests : response[0].redisplayStats.displayLimit shouldBe 11111 response[0].redisplayStats.displayDelay shouldBe 22222 - coVerify(exactly = 1) { mockHttpClient.get("apps/appId/subscriptions/subscriptionId/iams", any()) } + coVerify(exactly = 1) { mockHttpClient.get("apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", any()) } } test("listInAppMessages returns null when non-success response") { @@ -96,11 +96,11 @@ class InAppBackendServiceTests : val inAppBackendService = InAppBackendService(mockHttpClient, MockHelper.deviceService(), mockHydrator) // When - val response = inAppBackendService.listInAppMessages("appId", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) + val response = inAppBackendService.listInAppMessages("appId", "onesignal_id", "user123", "subscriptionId", RywData("123", 500L), mockSessionDurationProvider) // Then response shouldBe null - coVerify(exactly = 1) { mockHttpClient.get("apps/appId/subscriptions/subscriptionId/iams", any()) } + coVerify(exactly = 1) { mockHttpClient.get("apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", any()) } } test( @@ -125,7 +125,7 @@ class InAppBackendServiceTests : val inAppBackendService = InAppBackendService(mockHttpClient, MockHelper.deviceService(), mockHydrator) // When - val response = inAppBackendService.listInAppMessages("appId", "subscriptionId", RywData("1234", 500L), mockSessionDurationProvider) + val response = inAppBackendService.listInAppMessages("appId", "onesignal_id", "user123", "subscriptionId", RywData("1234", 500L), mockSessionDurationProvider) // Then response shouldNotBe null @@ -133,7 +133,7 @@ class InAppBackendServiceTests : coVerify(exactly = 1) { mockHttpClient.get( - "apps/appId/subscriptions/subscriptionId/iams", + "apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", match { it.rywToken == "1234" && it.retryCount == null && it.sessionDuration == mockSessionDurationProvider() }, @@ -143,7 +143,7 @@ class InAppBackendServiceTests : // Verify that the get method retried twice with the RYW token coVerify(exactly = 3) { mockHttpClient.get( - "apps/appId/subscriptions/subscriptionId/iams", + "apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", match { it.rywToken == "1234" && it.sessionDuration == mockSessionDurationProvider() && it.retryCount != null }, @@ -153,7 +153,7 @@ class InAppBackendServiceTests : // Verify that the get method was retried the final time without the RYW token coVerify(exactly = 1) { mockHttpClient.get( - "apps/appId/subscriptions/subscriptionId/iams", + "apps/appId/users/by/onesignal_id/user123/subscriptions/subscriptionId/iams", match { it.rywToken == null && it.sessionDuration == mockSessionDurationProvider() && it.retryCount == null }, diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt index 500b736e79..107e395d43 100644 --- a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt @@ -53,6 +53,7 @@ object MockHelper { configModel.foregroundFetchNotificationPermissionInterval = 1 configModel.appId = DEFAULT_APP_ID + configModel.useIdentityVerification = false if (action != null) { action(configModel) diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt index 8982aefc85..09a65333df 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt @@ -167,12 +167,13 @@ object OneSignalService { * Fetch user data from OneSignal API. * Note: This endpoint does not require authentication. * - * @param onesignalId The OneSignal user ID + * @param aliasLabel The alias type to look up by (e.g. "onesignal_id" or "external_id") + * @param aliasValue The alias value * @return UserData object containing aliases, tags, emails, and SMS numbers, or null on error */ - suspend fun fetchUser(onesignalId: String): UserData? = withContext(Dispatchers.IO) { - if (onesignalId.isEmpty()) { - LogManager.w(TAG, "Cannot fetch user - onesignalId is empty") + suspend fun fetchUser(aliasLabel: String, aliasValue: String, jwt: String? = null): UserData? = withContext(Dispatchers.IO) { + if (aliasValue.isEmpty()) { + LogManager.w(TAG, "Cannot fetch user - aliasValue is empty") return@withContext null } @@ -180,9 +181,9 @@ object OneSignalService { LogManager.w(TAG, "Cannot fetch user - appId not set") return@withContext null } - + try { - val url = "$ONESIGNAL_API_BASE_URL/apps/$appId/users/by/onesignal_id/$onesignalId" + val url = "$ONESIGNAL_API_BASE_URL/apps/$appId/users/by/$aliasLabel/$aliasValue" LogManager.d(TAG, "Fetching user data from: $url") val connection = (URL(url).openConnection() as HttpURLConnection).apply { @@ -190,6 +191,9 @@ object OneSignalService { connectTimeout = 30000 readTimeout = 30000 setRequestProperty("Accept", "application/json") + if (jwt != null) { + setRequestProperty("Authorization", "Bearer $jwt") + } requestMethod = "GET" } diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt index 70696e54fd..774b03fd97 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt @@ -19,12 +19,17 @@ class OneSignalRepository { } // User operations - suspend fun loginUser(externalUserId: String) = withContext(Dispatchers.IO) { - Log.d(TAG, "Logging in user with externalUserId: $externalUserId") - OneSignal.login(externalUserId) + suspend fun loginUser(externalUserId: String, jwtToken: String? = null) = withContext(Dispatchers.IO) { + Log.d(TAG, "Logging in user with externalUserId: $externalUserId, jwt: ${if (jwtToken != null) "provided" else "none"}") + OneSignal.login(externalUserId, jwtToken) Log.d(TAG, "Logged in user with onesignalId: ${OneSignal.User.onesignalId}") } + suspend fun updateUserJwt(externalUserId: String, jwtToken: String) = withContext(Dispatchers.IO) { + Log.d(TAG, "Updating JWT for externalUserId: $externalUserId") + OneSignal.updateUserJwt(externalUserId, jwtToken) + } + suspend fun logoutUser() = withContext(Dispatchers.IO) { Log.d(TAG, "Logging out user") OneSignal.logout() @@ -236,8 +241,8 @@ class OneSignalRepository { } // Fetch user data from API - suspend fun fetchUser(onesignalId: String): UserData? = withContext(Dispatchers.IO) { - Log.d(TAG, "Fetching user data for: $onesignalId") - OneSignalService.fetchUser(onesignalId) + suspend fun fetchUser(aliasLabel: String, aliasValue: String, jwt: String? = null): UserData? = withContext(Dispatchers.IO) { + Log.d(TAG, "Fetching user data by $aliasLabel: $aliasValue") + OneSignalService.fetchUser(aliasLabel, aliasValue, jwt) } } diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt index 8045664984..f4dcb99ba2 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt @@ -341,18 +341,60 @@ fun MultiSelectRemoveDialog( } /** - * Dialog for login/switch user. + * Dialog for login/switch user with optional JWT token. */ @Composable fun LoginDialog( onDismiss: () -> Unit, - onConfirm: (String) -> Unit + onConfirm: (String, String?) -> Unit ) { - SingleInputDialog( - title = "Login User", - label = "External User Id", - onDismiss = onDismiss, - onConfirm = onConfirm + var externalId by remember { mutableStateOf("") } + var jwtToken by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text("Login User", style = MaterialTheme.typography.titleMedium) + }, + text = { + Column { + OutlinedTextField( + value = externalId, + onValueChange = { externalId = it }, + label = { Text("External User Id") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + Spacer(modifier = Modifier.height(12.dp)) + OutlinedTextField( + value = jwtToken, + onValueChange = { jwtToken = it }, + label = { Text("JWT Token (optional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + } + }, + confirmButton = { + TextButton( + onClick = { onConfirm(externalId, jwtToken.ifBlank { null }) }, + enabled = externalId.isNotBlank() + ) { + Text("Login") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) ) } diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt index 82dfc01183..8eeeecc91b 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt @@ -69,6 +69,7 @@ fun MainScreen(viewModel: MainViewModel) { val consentRequired by viewModel.consentRequired.observeAsState(false) val privacyConsentGiven by viewModel.privacyConsentGiven.observeAsState(false) val externalUserId by viewModel.externalUserId.observeAsState() + val useIdentityVerification by viewModel.useIdentityVerification.observeAsState(false) val aliases by viewModel.aliases.observeAsState(emptyList()) val emails by viewModel.emails.observeAsState(emptyList()) val smsNumbers by viewModel.smsNumbers.observeAsState(emptyList()) @@ -80,6 +81,7 @@ fun MainScreen(viewModel: MainViewModel) { // Dialog states var showLoginDialog by remember { mutableStateOf(false) } + var showUpdateJwtDialog by remember { mutableStateOf(false) } var showAddAliasDialog by remember { mutableStateOf(false) } var showAddMultipleAliasDialog by remember { mutableStateOf(false) } var showAddEmailDialog by remember { mutableStateOf(false) } @@ -159,8 +161,11 @@ fun MainScreen(viewModel: MainViewModel) { // === USER SECTION === UserSection( externalUserId = externalUserId, + useIdentityVerification = useIdentityVerification, + onUseIdentityVerificationChange = { viewModel.setUseIdentityVerification(it) }, onLoginClick = { showLoginDialog = true }, - onLogoutClick = { viewModel.logoutUser() } + onLogoutClick = { viewModel.logoutUser() }, + onUpdateJwtClick = { showUpdateJwtDialog = true } ) // === PUSH SECTION === @@ -284,12 +289,25 @@ fun MainScreen(viewModel: MainViewModel) { if (showLoginDialog) { LoginDialog( onDismiss = { showLoginDialog = false }, - onConfirm = { userId -> - viewModel.loginUser(userId) + onConfirm = { userId, jwt -> + viewModel.loginUser(userId, jwt) showLoginDialog = false } ) } + + if (showUpdateJwtDialog) { + PairInputDialog( + title = "Update User JWT", + keyLabel = "External User Id", + valueLabel = "JWT Token", + onDismiss = { showUpdateJwtDialog = false }, + onConfirm = { externalId, token -> + viewModel.updateUserJwt(externalId, token) + showUpdateJwtDialog = false + } + ) + } if (showAddAliasDialog) { PairInputDialog( diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt index e65af736d2..46a7d4f50d 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt @@ -5,7 +5,9 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.OneSignal +import com.onesignal.UserJwtInvalidatedEvent import com.onesignal.notifications.IPermissionObserver import com.onesignal.sdktest.data.model.NotificationType import com.onesignal.sdktest.data.repository.OneSignalRepository @@ -19,7 +21,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class MainViewModel(application: Application) : AndroidViewModel(application), IPushSubscriptionObserver, IPermissionObserver, IUserStateObserver { +class MainViewModel(application: Application) : AndroidViewModel(application), IPushSubscriptionObserver, IPermissionObserver, IUserStateObserver, IUserJwtInvalidatedListener { private val repository = OneSignalRepository() @@ -74,6 +76,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I private val _locationShared = MutableLiveData() val locationShared: LiveData = _locationShared + // Identity Verification toggle (demo app only, controls alias used for API calls) + private val _useIdentityVerification = MutableLiveData() + val useIdentityVerification: LiveData = _useIdentityVerification + // Toast messages private val _toastMessage = MutableLiveData() val toastMessage: LiveData = _toastMessage @@ -99,6 +105,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I OneSignal.User.pushSubscription.addObserver(this) OneSignal.Notifications.addPermissionObserver(this) OneSignal.User.addObserver(this) + OneSignal.addUserJwtInvalidatedListener(this) android.util.Log.d("MainViewModel", "init: observers registered, current onesignalId=${OneSignal.User.onesignalId}") LogManager.debug("OneSignal ID: ${OneSignal.User.onesignalId ?: "not set"}") } @@ -127,6 +134,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I _privacyConsentGiven.value = repository.getPrivacyConsent() _inAppMessagesPaused.value = repository.isInAppMessagesPaused() _locationShared.value = repository.isLocationShared() + _useIdentityVerification.value = SharedPreferenceUtil.getCachedIdentityVerification(context) val externalId = OneSignal.User.externalId _externalUserId.value = if (externalId.isEmpty()) null else externalId @@ -145,16 +153,34 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I } fun fetchUserDataFromApi() { - val onesignalId = OneSignal.User.onesignalId - if (onesignalId.isNullOrEmpty()) { - _isLoading.value = false - return + val useIV = _useIdentityVerification.value == true + val aliasLabel: String + val aliasValue: String + + if (useIV) { + val externalId = _externalUserId.value + if (externalId.isNullOrEmpty()) { + _isLoading.value = false + return + } + aliasLabel = "external_id" + aliasValue = externalId + } else { + val onesignalId = OneSignal.User.onesignalId + if (onesignalId.isNullOrEmpty()) { + _isLoading.value = false + return + } + aliasLabel = "onesignal_id" + aliasValue = onesignalId } + val jwt = if (useIV) SharedPreferenceUtil.getCachedJwtToken(getApplication()) else null + _isLoading.value = true viewModelScope.launch(Dispatchers.IO) { try { - val userData = repository.fetchUser(onesignalId) + val userData = repository.fetchUser(aliasLabel, aliasValue, jwt) withContext(Dispatchers.Main) { if (userData != null) { aliasesList.clear() @@ -217,12 +243,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I private fun refreshTriggers() { _triggers.value = triggersList.toList() } // User operations - fun loginUser(externalUserId: String) { + fun loginUser(externalUserId: String, jwtToken: String? = null) { _isLoading.value = true viewModelScope.launch(Dispatchers.IO) { - repository.loginUser(externalUserId) + repository.loginUser(externalUserId, jwtToken) withContext(Dispatchers.Main) { SharedPreferenceUtil.cacheUserExternalUserId(getApplication(), externalUserId) + SharedPreferenceUtil.cacheJwtToken(getApplication(), jwtToken) _externalUserId.value = externalUserId showToast("Logged in as: $externalUserId") aliasesList.clear() @@ -235,7 +262,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I refreshTriggers() loadExistingTags() refreshPushSubscription() - // Loading stays on; onUserStateChange will call fetchUserDataFromApi() to dismiss it + _isLoading.value = false + } + } + } + + fun updateUserJwt(externalUserId: String, jwtToken: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.updateUserJwt(externalUserId, jwtToken) + withContext(Dispatchers.Main) { + SharedPreferenceUtil.cacheJwtToken(getApplication(), jwtToken) + showToast("Updated JWT for: $externalUserId") } } } @@ -262,6 +299,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I } } + fun setUseIdentityVerification(enabled: Boolean) { + SharedPreferenceUtil.cacheIdentityVerification(getApplication(), enabled) + _useIdentityVerification.value = enabled + showToast(if (enabled) "Identity verification enabled" else "Identity verification disabled") + } + // Consent required fun setConsentRequired(required: Boolean) { repository.setConsentRequired(required) @@ -619,8 +662,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I _pushEnabled.postValue(state.current.optedIn) } + override fun onUserJwtInvalidated(event: UserJwtInvalidatedEvent) { + LogManager.warn("JWT invalidated for externalId: ${event.externalId}") + showToast("JWT invalidated for: ${event.externalId}") + } + override fun onCleared() { super.onCleared() OneSignal.User.pushSubscription.removeObserver(this) + OneSignal.removeUserJwtInvalidatedListener(this) } } diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt index f672d322c1..7cc769d838 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt @@ -137,12 +137,22 @@ fun AppSection( @Composable fun UserSection( externalUserId: String?, + useIdentityVerification: Boolean, + onUseIdentityVerificationChange: (Boolean) -> Unit, onLoginClick: () -> Unit, - onLogoutClick: () -> Unit + onLogoutClick: () -> Unit, + onUpdateJwtClick: () -> Unit ) { val isLoggedIn = !externalUserId.isNullOrEmpty() SectionCard(title = "User") { + ToggleRow( + label = "Identity Verification", + description = "Use external_id for API calls", + checked = useIdentityVerification, + onCheckedChange = onUseIdentityVerificationChange + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) // Status Row( modifier = Modifier @@ -200,6 +210,11 @@ fun UserSection( onClick = onLogoutClick ) } + + OutlineButton( + text = "UPDATE USER JWT", + onClick = onUpdateJwtClick + ) } // === PUSH SECTION === diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt index f3b93dfb00..1cef40b592 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt @@ -12,6 +12,8 @@ object SharedPreferenceUtil { private const val LOCATION_SHARED_PREF = "LOCATION_SHARED_PREF" private const val IN_APP_MESSAGING_PAUSED_PREF = "IN_APP_MESSAGING_PAUSED_PREF" private const val CONSENT_REQUIRED_PREF = "CONSENT_REQUIRED_PREF" + private const val IDENTITY_VERIFICATION_PREF = "IDENTITY_VERIFICATION_PREF" + private const val JWT_TOKEN_PREF = "JWT_TOKEN_PREF" private fun getSharedPreference(context: Context): SharedPreferences { return context.getSharedPreferences(APP_SHARED_PREFS, Context.MODE_PRIVATE) @@ -69,4 +71,20 @@ object SharedPreferenceUtil { fun cacheConsentRequired(context: Context, required: Boolean) { getSharedPreference(context).edit().putBoolean(CONSENT_REQUIRED_PREF, required).apply() } + + fun getCachedIdentityVerification(context: Context): Boolean { + return getSharedPreference(context).getBoolean(IDENTITY_VERIFICATION_PREF, false) + } + + fun cacheIdentityVerification(context: Context, enabled: Boolean) { + getSharedPreference(context).edit().putBoolean(IDENTITY_VERIFICATION_PREF, enabled).apply() + } + + fun getCachedJwtToken(context: Context): String? { + return getSharedPreference(context).getString(JWT_TOKEN_PREF, null) + } + + fun cacheJwtToken(context: Context, token: String?) { + getSharedPreference(context).edit().putString(JWT_TOKEN_PREF, token).apply() + } }