Skip to content

Add QPurchaseResult to EntitlementsUpdateListener#769

Closed
NickSxti wants to merge 2 commits into
developfrom
nch/sup3-58-entitlements-listener-purchase-result
Closed

Add QPurchaseResult to EntitlementsUpdateListener#769
NickSxti wants to merge 2 commits into
developfrom
nch/sup3-58-entitlements-listener-purchase-result

Conversation

@NickSxti
Copy link
Copy Markdown
Contributor

Summary

  • Extends QEntitlementsUpdateListener with a new overloaded onEntitlementsUpdated(entitlements, purchaseResult) method
  • When deferred purchases complete in background (no active callback), the listener now receives QPurchaseResult with full purchase details
  • Fixes the issue where consumable purchases that don't create entitlements resulted in an empty map with no transaction info (1768+ instances in API Gateway logs)

Changes

  • QEntitlementsUpdateListener: Added second method with QPurchaseResult? parameter. Both methods have default implementations for mutual delegation — fully backward compatible
  • QProductCenterManager: Success path (line ~1047) and fallback path (calculatePurchasePermissionsLocally) now pass QPurchaseResult to listener when no purchase callback exists
  • Sample app: Updated EntitlementsFragment to demonstrate the new method with consumable purchase handling
  • Tests: Added tests verifying listener receives QPurchaseResult for deferred purchases, and is NOT called when a callback exists

Backward compatibility

  • Old implementations (override only old method) continue to work — new method default calls old
  • New implementations (override only new method) work — old method default calls new
  • @Deprecated annotation guides new adopters to the new method
  • Java interop preserved — both method signatures accessible from Java

Test plan

  • Verify SDK compiles: ./gradlew :sdk:assemble
  • Run unit tests: ./gradlew :sdk:test
  • Verify sample app compiles without changes to old listener code
  • Test deferred consumable purchase flow — listener should receive non-null QPurchaseResult
  • Test normal purchase flow — callback receives result, listener is NOT called

Resolves: SUP3-58

🤖 Generated with Claude Code

@NickSxti
Copy link
Copy Markdown
Contributor Author

Review & Fix: Mutual Recursion in QEntitlementsUpdateListener

Problem Found

Both interface methods had default implementations that delegated to each other:

// Before (dangerous):
fun onEntitlementsUpdated(entitlements) {
    onEntitlementsUpdated(entitlements, null)  // → calls 2-arg
}

fun onEntitlementsUpdated(entitlements, purchaseResult?) {
    onEntitlementsUpdated(entitlements)  // → calls 1-arg → StackOverflowError
}

If a consumer implemented the interface without overriding either method → infinite recursion → StackOverflowError crash.

Fix Applied (commit 745db80)

Replaced the 1-arg default body with a no-op:

// After (safe):
fun onEntitlementsUpdated(entitlements) {
    // No-op default. Overridden by existing consumers.
}

fun onEntitlementsUpdated(entitlements, purchaseResult?) {
    onEntitlementsUpdated(entitlements)  // → calls 1-arg → no-op (terminates)
}

Backward Compatibility Matrix

Scenario SDK calls 2-arg → Result
Old code (overrides 1-arg only) 2-arg default → 1-arg override ✅ Works
New code (overrides 2-arg only) 2-arg override runs ✅ Works
Both overridden 2-arg override runs ✅ Works
Neither overridden 2-arg default → 1-arg no-op ✅ Silent no-op (no crash)

Note on local tests

Could not run ./gradlew :sdk:test locally — requires JDK 11+ (only JDK 8 available on this machine). Please verify CI passes. The change is a 1-line no-op replacement with no logic changes to production flow.

🤖 Generated with Claude Code

@NickSxti
Copy link
Copy Markdown
Contributor Author

Local Test Results ✅

Environment: JDK 17.0.18 (Temurin), Android SDK, macOS (aarch64)

./gradlew :sdk:testDebugUnitTest — All 27 tests PASSED

QHandledPurchasesCacheTest > sequential saving test PASSED
QHandledPurchasesCacheTest > non empty cache PASSED
QHandledPurchasesCacheTest > empty cache PASSED
QHandledPurchasesCacheTest > saving multiple purchases PASSED
QProductCenterManagerTest > normal purchase with callback does not notify listener PASSED
QProductCenterManagerTest > handle pending purchases when launching is finished and query purchases failed PASSED
QProductCenterManagerTest > deferred purchase with no callback notifies listener with purchaseResult PASSED
QProductCenterManagerTest > handle pending purchases when launching is finished and query purchases completed PASSED
QProductCenterManagerTest > handle pending purchases when launching is not finished PASSED
QUserPropertiesManagerTest > should not force send properties when properties storage is empty PASSED
QUserPropertiesManagerTest > send properties with delay on background PASSED
QUserPropertiesManagerTest > should set and not send user property when sending properties is scheduled PASSED
QUserPropertiesManagerTest > should not send facebook attribution when it is null PASSED
QUserPropertiesManagerTest > should force send properties and get response in onError callback PASSED
QUserPropertiesManagerTest > should force send properties and get response in onSuccess callback PASSED
QUserPropertiesManagerTest > should send facebook attribution when it is not null PASSED
QUserPropertiesManagerTest > on app foreground when properties is not empty PASSED
QUserPropertiesManagerTest > on app foreground when properties is empty PASSED
QUserPropertiesManagerTest > should set isRequestInProgress to true and isSendingScheduled to false when properties storage is not empty PASSED
QUserPropertiesManagerTest > setProperty PASSED
QUserPropertiesManagerTest > should force send properties when onAppBackground is called PASSED
QUserPropertiesManagerTest > should force send properties again after failed attempt on foreground PASSED
QUserPropertiesManagerTest > send properties with delay on foreground PASSED
QUserPropertiesManagerTest > should not set user property when its value is empty PASSED
QUserPropertiesManagerTest > should not force send properties again after failed attempt on background PASSED
QUserPropertiesManagerTest > should not force send properties when request is in progress PASSED
QUserPropertiesManagerTest > should set and send user property when it is not empty and sending is not scheduled PASSED
AppRequestTest > appRequestWithCorrectData PASSED
OsRequestTest > osRequestWithCorrectData PASSED
ProviderDataRequestTest > providerDataRequestWithCorrectData PASSED
UserPropertiesStorageTest > saveAndClearProperties PASSED
UserPropertiesStorageTest > saveProperties PASSED

./gradlew :sdk:assemble — BUILD SUCCESSFUL

SDK compiles without errors for both debug and release variants.

Fix applied in commit 745db80

Replaced mutual recursion in QEntitlementsUpdateListener with no-op default for the deprecated 1-arg method.

🤖 Generated with Claude Code

NickChechnev and others added 2 commits March 5, 2026 15:12
…ases

When deferred consumable purchases complete in background (no active
callback), the SDK now passes QPurchaseResult to the entitlements
update listener. This allows developers to access purchase details
for consumables that don't create entitlements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the 1-arg method's default body (which delegated to 2-arg)
with a no-op. This breaks the mutual recursion cycle that would cause
StackOverflowError if neither method was overridden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@NickSxti NickSxti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review (LEVER)

Logic: When deferred/background purchases complete without an active callback, the previous onEntitlementsUpdated(entitlements) delivered only entitlements. Consumable purchases that don't create entitlements produced an empty map and no transaction info (1768+ instances in API Gateway logs). Fix adds overloaded onEntitlementsUpdated(entitlements, purchaseResult) with a QPurchaseResult? argument; QProductCenterManager success path (~line 1047) and calculatePurchasePermissionsLocally fallback now pass QPurchaseResult to the listener when no purchase callback exists.

Edge cases:

  • Listener overrides only old method: default impl delegates to old — unchanged behavior.
  • Listener overrides only new method: default impl delegates to new — unchanged behavior.
  • Listener overrides both: direct invocation, no re-entry (confirm tests assert no double-dispatch).
  • Active purchase callback present: listener is NOT called (unchanged) — test covers.
  • Deferred purchase with null result from store: listener receives null QPurchaseResult via nullable param.
  • Java interop: both method signatures remain accessible.

Verification:

  • Tests: listener receives QPurchaseResult for deferred purchases; listener not called when callback exists.
  • CI: ./gradlew :sdk:assemble + :sdk:test pending.
  • Sample app updated to demonstrate consumable flow.
  • Manual test plan for deferred consumable + normal purchase flow pending.

Effects / blast radius:

  • Public API surface grows by one overload — non-breaking due to mutual-delegation defaults.
  • @Deprecated on old method guides new adopters.
  • Two internal call sites updated.

Risks / rollback:

  • Revert removes overload — any consumer that adopted the new signature would break; ensure iOS-sdk#644 ships in lockstep for parity.
  • Mutual delegation must not infinite-loop — tests must explicitly assert single invocation for each override combination.
  • Consumer migration is gradual because old method stays functional.

@NickSxti
Copy link
Copy Markdown
Contributor Author

Superseded by #785 (QDeferredPurchasesListener, shipped in SDK 9.3.0 on 2026-04-06). The overload approach was replaced with a dedicated listener interface; QEntitlementsUpdateListener is now @Deprecated on main pointing to QDeferredPurchasesListener. Closing - no longer the intended direction.

@NickSxti NickSxti closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants