Skip to content

Restore Purchases Returns Empty Entitlements for Valid Google Play Purchases #733

@wesjon

Description

@wesjon

Issue Summary

The Qonversion.shared.restore() method is returning empty entitlements for users who have valid, active purchases through Google Play. This issue started on October 28, 2025 and has affected approximately 30 users who have contacted support (probably more). On this date I migrated to the new One-Time products from Google Play which are supposed to be supported and be backwards compatible.

Image

Impact

  • Severity: Critical - Paying customers cannot access premium features they purchased
  • Affected Users: ~30 users (and growing) who purchased through Google Play
  • Scenarios:
    • Users who clear app data and try to restore purchases
    • Users who install the app on a new device with the same Google account
  • Purchase Flow: Initial purchases work correctly, but restoration fails (success in the callback restore but empty entitlements)

Environment

  • Qonversion SDK Version: 8.2.6 and 9.0.2
  • Platform: Android
  • Min SDK: 24
  • Target SDK: 36
  • Compile SDK: 36
  • Kotlin: 2.2.21
  • Build Environment: Production (tested and confirmed)
  • Launch Mode: QLaunchMode.SubscriptionManagement

SDK Initialization

private fun setupBilling() {
    Qonversion.initialize(
        QonversionConfig.Builder(
            this,
            "<MY KEY>",
            QLaunchMode.SubscriptionManagement,
        )
            .setEnvironment(if (BuildConfig.DEBUG) QEnvironment.Sandbox else QEnvironment.Production)
            .setFallbackFileIdentifier(R.raw.qonversion_android_fallbacks)
            .build(),
    )

    tryOrLog {
        Qonversion.shared.syncPurchases()
        Qonversion.shared.products(object : QonversionProductsCallback {
            override fun onError(error: QonversionError) {
                Timber.e("Qonversion error: ${error.description}")
            }

            override fun onSuccess(products: Map<String, QProduct>) {
                Timber.d("Products fetched successfully. Count: ${products.size}")
            }
        })
    }

    FirebaseAnalytics.getInstance(this).appInstanceId.addOnCompleteListener { task ->
        task.result?.let { appInstanceId ->
            Qonversion.shared.setUserProperty(
                QUserPropertyKey.FirebaseAppInstanceId,
                appInstanceId,
            )
        }
    }
}

Restore Purchase Implementation

// Extension function wrapper
suspend fun Qonversion.restorePurchases(): QEntitlement? =
        suspendCoroutine { continuation ->
                this.restore(
                    object : QonversionEntitlementsCallback {
                        override fun onError(error: QonversionError) {
                            continuation.resume(null)
                        }

                        override fun onSuccess(entitlements: Map<String, QEntitlement>) {
                            // Here I validate if entitlement are active, but it's not returning anything in the map
                            continuation.resume(entitlements.values.find {
                                it.hasPremiumEntitlement(validateActive = true)
                            })
                        }
                    },
                )
        }

// Repository method
override suspend fun restorePurchases(): Boolean {
    try {
        val hasRestoredPurchases =
            Qonversion.shared.restorePurchases().hasPremiumEntitlement()
    } catch (ex: Throwable) {
        Timber.e(ex)
        return false
    }
}

// Entitlement validation
fun QEntitlement?.hasPremiumEntitlement(validateActive: Boolean = true): Boolean {
    val isPremiumEntitlement = this?.id in premiumEntitlements
    val isValid = if (validateActive) {
        this?.isActive == true
    } else {
        true
    }
    return isPremiumEntitlement && isValid
}

Expected Behavior

When a user with a valid Google Play purchase calls restore():

  1. The SDK should communicate with Google Play Billing
  2. Retrieve the user's active subscriptions/purchases
  3. Return a non-empty entitlements map containing the user's premium entitlement
  4. The app should successfully restore premium access

Actual Behavior

When restore() is called:

  1. The onSuccess callback is triggered (not onError)
  2. The entitlements map is completely empty ({})
  3. Production logs confirm: restorePurchases#onSuccess: {}
  4. Users cannot access premium features they legitimately purchased
  5. This happens consistently for affected users across app reinstalls and device changes

Steps to Reproduce

  1. Purchase a premium subscription/product through Google Play (this works)
  2. Either:
    • Clear the app data in Android settings
    • OR install the app on a new device using the same Google account
  3. Open the app and tap "Restore Purchases"
  4. Observe: Restore returns empty entitlements despite valid Google Play purchase

Timeline

  • Before October 28, 2025: Restore purchases worked correctly
  • October 28, 2025: Issue started - restore began returning empty entitlements, One Time Product Migrated on Google Play
  • October 31, 2025: Temporarily upgraded to SDK 9.0.2 to attempt fix
  • November 2, 2025: Reverted to 8.2.6 as 9.0.2 had API breaking changes
  • No code changes to our restore logic during this period

Additional Context

  • The initial purchase flow works perfectly - users can successfully purchase and receive entitlements
  • Only the restore functionality is broken
  • Issue is consistent and reproducible in production environment
  • The restore() callback does not error - it succeeds but returns empty data
  • We have implemented comprehensive logging which confirms empty entitlements map
  • We also call syncPurchases() during initialization, but this doesn't resolve the restore issue

Can you help me with this issue? Is there something else I should be doing in order to be able to restore user's purchases?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions