From f654fbddff82d6acb9bc4202a67f2bf8e27b4c52 Mon Sep 17 00:00:00 2001 From: Nikita Ushakov Date: Thu, 12 Feb 2026 10:40:08 +0700 Subject: [PATCH 1/2] Add user switching support in restore flow When restore returns a different uid than the current local user (e.g. after app reinstall), the SDK now detects this and updates the local user state accordingly: persists the new user ID, updates internal config, resets remote config cache, and clears the permissions cache. This ensures the SDK correctly returns to the original user after a restore instead of creating a duplicate user with copied entitlements. Co-authored-by: Cursor --- .../sdk/internal/QProductCenterManager.kt | 19 ++++ .../sdk/internal/QProductCenterManagerTest.kt | 105 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt index 2bacf99a..e50a9f50 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt @@ -424,6 +424,7 @@ internal class QProductCenterManager internal constructor( requestTrigger, object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { + handleUserSwitchingOnRestore(launchResult) updateLaunchResult(launchResult) executeRestoreBlocksOnSuccess(launchResult.permissions.toEntitlementsMap()) } @@ -487,6 +488,24 @@ internal class QProductCenterManager internal constructor( // Private functions + private fun handleUserSwitchingOnRestore(launchResult: QLaunchResult) { + val currentUserId = userInfoService.obtainUserId() + val newUserId = launchResult.uid + + if (newUserId.isEmpty() || newUserId == currentUserId) { + return + } + + logger.release( + "restore() -> User switch detected. Switching from $currentUserId to $newUserId" + ) + + userInfoService.storeQonversionUserId(newUserId) + internalConfig.uid = newUserId + remoteConfigManager.onUserUpdate() + launchResultCache.clearPermissionsCache() + } + private fun calculateRestorePermissionsLocally( purchases: List, restoreError: QonversionError diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt index 0535016a..fe985ce0 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt @@ -7,7 +7,11 @@ import android.os.Build import com.android.billingclient.api.BillingClient import com.android.billingclient.api.Purchase import com.qonversion.android.sdk.dto.QPurchaseOptions +import com.qonversion.android.sdk.dto.QonversionError +import com.qonversion.android.sdk.dto.QonversionErrorCode +import com.qonversion.android.sdk.listeners.QonversionEntitlementsCallback import com.qonversion.android.sdk.listeners.QonversionLaunchCallback +import com.qonversion.android.sdk.internal.api.RequestTrigger import com.qonversion.android.sdk.internal.billing.BillingError import com.qonversion.android.sdk.internal.billing.QonversionBillingService import com.qonversion.android.sdk.internal.dto.QLaunchResult @@ -153,6 +157,107 @@ internal class QProductCenterManagerTest { ) } + // User switching on restore tests + + @Test + fun `restore with same uid should not trigger user switch`() { + val currentUid = "user_old" + val launchResult = QLaunchResult(currentUid, Date(), offerings = null) + + every { mockUserInfoService.obtainUserId() } returns currentUid + mockRestoreFlow(launchResult) + + val callback = mockk(relaxed = true) + productCenterManager.restore(RequestTrigger.Restore, callback) + + verify(exactly = 0) { mockUserInfoService.storeQonversionUserId(any()) } + verify(exactly = 0) { mockRemoteConfigManager.onUserUpdate() } + verify(exactly = 0) { mockLaunchResultCacheWrapper.clearPermissionsCache() } + verify { callback.onSuccess(any()) } + } + + @Test + fun `restore with different uid should trigger user switch`() { + val currentUid = "user_new" + val originalOwnerUid = "user_old" + val launchResult = QLaunchResult(originalOwnerUid, Date(), offerings = null) + + every { mockUserInfoService.obtainUserId() } returns currentUid + mockRestoreFlow(launchResult) + + val callback = mockk(relaxed = true) + productCenterManager.restore(RequestTrigger.Restore, callback) + + verifyOrder { + mockUserInfoService.storeQonversionUserId(originalOwnerUid) + mockConfig.uid = originalOwnerUid + mockRemoteConfigManager.onUserUpdate() + mockLaunchResultCacheWrapper.clearPermissionsCache() + } + verify { callback.onSuccess(any()) } + verify { mockLogger.release(match { it.contains("User switch detected") }) } + } + + @Test + fun `restore with error should not trigger user switch`() { + every { mockBillingService.queryPurchases(any(), captureLambda()) } answers { + lambda<(List) -> Unit>().captured.invoke( + listOf(mockPurchase(Purchase.PurchaseState.PURCHASED, false)) + ) + } + every { mockBillingService.consumePurchases(any()) } just Runs + + val callbackSlot = slot() + every { + mockRepository.restore(any(), any(), any(), capture(callbackSlot)) + } answers { + callbackSlot.captured.onError( + QonversionError(QonversionErrorCode.BackendError) + ) + } + + val callback = mockk(relaxed = true) + productCenterManager.restore(RequestTrigger.Restore, callback) + + verify(exactly = 0) { mockUserInfoService.storeQonversionUserId(any()) } + verify(exactly = 0) { mockRemoteConfigManager.onUserUpdate() } + verify(exactly = 0) { mockLaunchResultCacheWrapper.clearPermissionsCache() } + verify { callback.onError(any()) } + } + + @Test + fun `restore with empty uid in response should not trigger user switch`() { + val currentUid = "user_current" + val launchResult = QLaunchResult("", Date(), offerings = null) + + every { mockUserInfoService.obtainUserId() } returns currentUid + mockRestoreFlow(launchResult) + + val callback = mockk(relaxed = true) + productCenterManager.restore(RequestTrigger.Restore, callback) + + verify(exactly = 0) { mockUserInfoService.storeQonversionUserId(any()) } + verify(exactly = 0) { mockRemoteConfigManager.onUserUpdate() } + verify(exactly = 0) { mockLaunchResultCacheWrapper.clearPermissionsCache() } + verify { callback.onSuccess(any()) } + } + + private fun mockRestoreFlow(launchResult: QLaunchResult) { + every { mockBillingService.queryPurchases(any(), captureLambda()) } answers { + lambda<(List) -> Unit>().captured.invoke( + listOf(mockPurchase(Purchase.PurchaseState.PURCHASED, false)) + ) + } + every { mockBillingService.consumePurchases(any()) } just Runs + + val callbackSlot = slot() + every { + mockRepository.restore(any(), any(), any(), capture(callbackSlot)) + } answers { + callbackSlot.captured.onSuccess(launchResult) + } + } + private fun mockPurchase( @Purchase.PurchaseState purchaseState: Int, isAcknowledged: Boolean From 586877f84cf1f82a12062b08dfc7621da408822e Mon Sep 17 00:00:00 2001 From: Nikita Ushakov Date: Thu, 12 Feb 2026 21:59:23 +0700 Subject: [PATCH 2/2] Change user switch log level from release to debug Co-authored-by: Cursor --- .../qonversion/android/sdk/internal/QProductCenterManager.kt | 2 +- .../android/sdk/internal/QProductCenterManagerTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt index e50a9f50..782fd2c9 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt @@ -496,7 +496,7 @@ internal class QProductCenterManager internal constructor( return } - logger.release( + logger.debug( "restore() -> User switch detected. Switching from $currentUserId to $newUserId" ) diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt index fe985ce0..de6e44bb 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt @@ -195,7 +195,7 @@ internal class QProductCenterManagerTest { mockLaunchResultCacheWrapper.clearPermissionsCache() } verify { callback.onSuccess(any()) } - verify { mockLogger.release(match { it.contains("User switch detected") }) } + verify { mockLogger.debug(match { it.contains("User switch detected") }) } } @Test