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..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 @@ -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.debug( + "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..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 @@ -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.debug(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