Skip to content

Refactor lifecycle seams for reward verification locator#3475

Open
polmiro wants to merge 21 commits into
admob-reward-verificationfrom
admob-reward-verification-locator-split
Open

Refactor lifecycle seams for reward verification locator#3475
polmiro wants to merge 21 commits into
admob-reward-verificationfrom
admob-reward-verification-locator-split

Conversation

@polmiro
Copy link
Copy Markdown
Member

@polmiro polmiro commented May 13, 2026

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-ios and hybrids

Motivation

Provide the lifecycle infrastructure needed by the AdMob reward verification locator while keeping reward verification behavior implementation out of scope for this PR.

Description

  • add PurchasesLifecycleListener and PurchasesLifecycleEventBus in :purchases to publish configure/close lifecycle events
  • add PurchasesLifecycleListeners to centralize default lifecycle forwarding
  • wire both defaults and custom-entitlement Purchases variants to notify lifecycle events on configure/close
  • add RewardVerificationServiceLocator lifecycle hook registration/dispatch plumbing in feature:admob
  • update locator dispatch to snapshot hooks under synchronization and invoke callbacks outside the lock
  • add focused tests for lifecycle bus behavior, lifecycle wiring (defaults + custom entitlement), and service locator registration/dispatch + concurrency regression behavior
  • keep reward verification setup/dispatcher/poller implementation out of scope for this PR

Note

Medium Risk
Touches core Purchases.configure/close wiring (defaults + custom entitlement modes), so mistakes could cause missed callbacks or ordering/concurrency issues, though behavior is covered by new unit tests and scoped to internal APIs.

Overview
Adds an internal lifecycle notification seam to purchases by introducing PurchasesLifecycleListener plus a synchronized PurchasesLifecycleEventBus that broadcasts configure and close events (including replaying the latest configured instance to newly-registered listeners).

Wires both Purchases.configure(...) and Purchases.configureInCustomEntitlementsComputationMode(...) to emit onPurchasesConfigured, and both Purchases.close() implementations to emit onPurchasesClosed before delegating to the orchestrator.

In feature:admob, introduces RewardVerificationServiceLocator that registers exactly once to the lifecycle listener and fan-outs lifecycle callbacks to a snapshot of registered hooks to avoid deadlocks/reentrancy issues. Adds focused unit tests for the event bus semantics, Purchases wiring, and locator concurrency/registration behavior.

Reviewed by Cursor Bugbot for commit 2b0b876. Bugbot is set up for automated code reviews on this repo. Configure here.

polmiro added 4 commits May 13, 2026 14:41
…t surface.

This aligns the core endpoint response mapping with reward payload support and removes unlaunched compatibility layers so callers consume one consistent result model.
This aligns endpoint and response transport naming with the result-based SDK surface while preserving the same backend path and behavior.
This keeps behavior unchanged while reducing return count in reward payload mapping.
Unexpected non-object reward payloads now short-circuit to unsupported reward instead of flowing through type parsing.
@polmiro polmiro changed the title refactor: make reward verification services instance-scoped Reward verification services instance-scoped May 13, 2026
@polmiro polmiro changed the base branch from admob-reward-verification-adapter-core-integration to admob-reward-verification May 13, 2026 15:31
@polmiro polmiro force-pushed the admob-reward-verification-locator-split branch from 46b17a1 to 2d2b0f9 Compare May 13, 2026 15:35
@polmiro polmiro changed the base branch from admob-reward-verification to admob-reward-verification-response-surface May 13, 2026 15:39
@polmiro polmiro force-pushed the admob-reward-verification-locator-split branch from 2d2b0f9 to bdb926c Compare May 13, 2026 15:44
Add purchases lifecycle notifications in core and introduce an AdMob reward verification service locator hook surface, while deferring setup/dispatcher/poller implementation to the stacked follow-up PR.
@polmiro polmiro force-pushed the admob-reward-verification-locator-split branch from bdb926c to 94a1b87 Compare May 13, 2026 15:46
@polmiro polmiro changed the title Reward verification services instance-scoped Add lifecycle hooks for reward verification locator May 13, 2026
polmiro added 3 commits May 13, 2026 17:53
Add focused tests for Purchases lifecycle event dispatching, close-to-event-bus wiring, and reward verification locator hook registration/forwarding behavior.
Add focused lifecycle wiring assertions for both defaults and custom entitlement configure paths, and trim an unnecessary experimental opt-in in the AdMob reward verification service locator.
Switch reward verification lifecycle wiring to instance-owned collaborators and add a small lifecycle listener seam in Purchases so tests can verify behavior without static event-bus mocking.
@polmiro polmiro changed the title Add lifecycle hooks for reward verification locator Refactor lifecycle seams for reward verification locator May 13, 2026
Deduplicate Purchases lifecycle listener forwarding by using a shared default provider across flavor-specific Purchases entrypoints.
@polmiro polmiro requested a review from tonidero May 13, 2026 16:46
@polmiro polmiro marked this pull request as ready for review May 13, 2026 16:46
@polmiro polmiro requested a review from a team as a code owner May 13, 2026 16:46
@polmiro
Copy link
Copy Markdown
Member Author

polmiro commented May 13, 2026

@tonidero this is a preliminary PR that implements the service locator in order to do clean ups for reward verifications. Let me know what do you think, hopefully its in the lines of what you were thinking about!

Comment thread purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesLifecycleEventBus.kt Outdated
Comment thread purchases/src/test/java/com/revenuecat/purchases/PurchasesLifecycleWiringTest.kt Outdated
polmiro added 3 commits May 14, 2026 10:55
Replace singleton lifecycle bus usage with an internal instance-based bus, tighten visibility/opt-in usage, and keep listener state transitions coordinated through synchronized helpers.
Avoid invoking lifecycle hook callbacks while holding the ServiceLocator monitor, and add a regression test that verifies reentrant hook mutation does not deadlock while preserving snapshot dispatch semantics.
Prevent stale configured callbacks after close by synchronizing lifecycle bus operations and replace the probabilistic race test with deterministic ordering coverage.
listenersToNotify.forEach { listener ->
listener.onPurchasesConfigured(purchases)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Event bus invokes listener callbacks inside synchronized block

Medium Severity

PurchasesLifecycleEventBus.onConfigured and onClosed are fully @Synchronized, invoking all listener callbacks while holding the event bus lock. This contradicts the PR's stated "snapshot under synchronization and invoke callbacks outside the lock" pattern applied to RewardVerificationServiceLocator. If any listener's callback blocks on cross-thread work that needs to call register/unregister on the event bus, a deadlock will occur — the exact scenario the locator's own test validates against.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 10bf442. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If we want both properties (strict ordering + non-locking callbacks), that likely needs a larger redesign rather than local synchronization tweaks. Wondering how big of a concern is this and what are some possible lightweight ways to address it.

polmiro added 3 commits May 14, 2026 13:30
Document why lifecycle callbacks are dispatched under the bus lock to preserve ordering guarantees, and remove incorrect unused-parameter suppressions in reward verification lifecycle hooks.
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ac9f91e. Configure here.

Base automatically changed from admob-reward-verification-response-surface to admob-reward-verification May 18, 2026 17:56
import com.revenuecat.purchases.PurchasesLifecycleListener

internal interface RewardVerificationLifecycleHook {
fun onPurchasesConfigured(purchases: Purchases)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm the part that I'm failing to see is how RewardVerificationServiceLocator is actually used/registered... I think this is indeed pretty close to what I meant, but to make it clearer, this is what I thought:

  • The purchases SDK would add a Service interface and a "Purchase.registerService" static method (both marked as @InternalRevenueCatAPI). The Service interface would have initialize and close methods of some kind, possibly passing in the context, or whatever dependency is needed. I think what we're missing in this PR is the registerService-like method? In this PR, the Service is equivalent to the PurchasesLifecycleListener.
  • The admob SDK can implement an instance of this Service with an initialize/close implementations that would actually perform the initialization/cleanup. We have this part mostly I think.
  • The purchases SDK would call initialize/close where appropriate.

So basically what you have here is pretty close, but I'm just missing the part where this is actually "registered". Lmk if I'm missing something though! Just want to make sure we're on the same page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants