fix: SUP3-118 defer consumePurchases when product types unknown#797
Merged
Merged
Conversation
When restore() or handlePurchases() runs before launch() completes (e.g., app calls restore immediately after SDK init on fresh install), getNonConsumableStoreIds() returns emptySet() because the product cache is not yet populated. This causes ALL in-app purchases to be consumed, permanently destroying Lifetime purchases. Fix: getNonConsumableStoreIds() now returns null (not emptySet()) when products are unavailable. restore() and handlePurchases() use a hybrid approach - consume immediately if products are known (preserving current behavior), or defer to onSuccess callback where fresh product types from the API response are available. If the API call fails and products are still unknown, no consume happens - the purchase stays safe in Google Play and will be processed on the next successful launch. The launch onSuccess path (line 807) already followed this pattern correctly (updateLaunchResult before consumePurchases). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NickSxti
commented
Apr 15, 2026
Contributor
Author
NickSxti
left a comment
There was a problem hiding this comment.
Self-review (LEVER)
Logic: Race in restore() and handlePurchases(): when called before launch() completes, getNonConsumableStoreIds() returned emptySet() (product cache empty), so consumePurchases treated all in-app purchases as consumable and permanently destroyed Lifetime purchases via Google consume(). Regressed in 1d4fd73 (DEV-589). Fix: return null when products aren't loaded; hybrid — consume immediately if loaded (preserves current behavior), defer to launch onSuccess otherwise; never consume on API error when unknown.
Edge cases:
- Fresh install + restore before launch: products unknown → defer (was: consume Lifetime).
- Products loaded + restore: consume immediately (unchanged).
- API error + products unknown: no consume; purchase stays safe in Google Play, recovered on next launch.
- Session cache cleared by
resetSessionCache(): same path as fresh install, covered. - Matches the
isLaunchingFinishedguard pattern already used byhandlePendingPurchases()and launchonSuccess.
Verification:
- Tests planned for: restore + products-not-loaded → deferred; restore + products-loaded → immediate; restore + API error + products-not-loaded → no-op; handlePurchases + products-not-loaded → deferred.
- Existing restore and handlePendingPurchases tests must stay green.
- Repro evidence: ES logs show restore arriving 686ms before init for the same client.
Effects / blast radius:
- Two call sites:
restore()(~line 419) andhandlePurchases()(~line 1032). - No public API change.
- Behavior shift only for callers inside the pre-launch race window.
Risks / rollback:
- Pre-fix worst case: irreversible Lifetime destruction. Post-fix worst case: short consume deferral.
- Revert restores the bug; safe to roll forward.
- Test boxes in the PR body are still unchecked - confirm all pass before merge.
SpertsyanKM
approved these changes
Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
restore()andhandlePurchases()consume Lifetime purchases when called beforelaunch()completesgetNonConsumableStoreIds()now returnsnull(notemptySet()) when products aren't loadedonSuccessif notRoot Cause
getNonConsumableStoreIds()returnedemptySet()whenlaunchResultCache.getActualProducts()was null (fresh install, session cleared byresetSessionCache()). This causedconsumePurchasesto treat ALL in-app purchases as consumable, permanently destroying Lifetime purchases via Google'sconsume().Two unguarded call sites:
restore()(line 419) andhandlePurchases()(line 1032). Compare withhandlePendingPurchases()which correctly guards withisLaunchingFinished, and launchonSuccesswhich correctly runsupdateLaunchResultbeforeconsumePurchases.Introduced by commit 1d4fd73 (DEV-589).
Evidence
ES logs show restore request arriving 686ms before init for the same client - confirming restore ran before launch completed.
Linear
SUP3-118
Test plan
Generated with Claude Code