feat: first-class intro pricing APIs#1
Conversation
Symmetric with the existing free-trial surface so "$1 first week, then $19/year" and similar offers are a one-liner for paywall code. - SubscriptionSpec.withIntro(productId, basePlanId, introOfferId) sugar - PlayBillingWrapper.isIntroEligible / getIntroPhase / getIntroPeriodIso / getIntroEndMillis (+ multi-spec scan overload) - getIntroPrice / getRecurringPrice disambiguate getFormattedPrice, which returns the first non-trial phase (= intro price when an intro offer is selected) and left no accessor for the renewal price - OfferSelector.isIntroEligible static helper (Play omits ineligible offers, so presence of FINITE_RECURRING paid phase = eligibility signal) - BillingAnalytics.onIntroStarted default hook, mutex with onTrialStarted on first-time delivery; includes billingCycleCount for multi-cycle intros - Tests: +8 cases across OfferSelectorTest + SubscriptionSpecTest - Docs: README scenarios + API tables + analytics block, GUIDE scenario 4 rewritten to use new helpers
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds first-class intro-pricing support: eligibility and phase discovery APIs, formatted price helpers, client-side intro-end estimation, a SubscriptionSpec helper to prefer intro offers, emission of an onIntroStarted analytics hook, and corresponding docs and tests. ChangesIntro pricing feature
Sequence Diagram(s)sequenceDiagram
participant UI as Client / UI
participant Wrapper as PlayBillingWrapper
participant Selector as OfferSelector
participant Play as Play Billing
participant Analytics as BillingAnalytics
UI->>Wrapper: isIntroEligible(productId, basePlanId) / getIntroPrice(...)
Wrapper->>Selector: findOfferWithIntroPhase(productDetails, basePlanId)
Selector-->>Wrapper: SubscriptionOfferDetails (intro phase)
Wrapper-->>UI: formatted price / eligibility
UI->>Play: subscribe(spec with preferredOfferId?)
Play-->>Wrapper: PurchaseInfo (first-time subscription)
Wrapper->>Selector: resolve chosen offer & intro phase
alt intro phase resolved
Wrapper->>Analytics: onIntroStarted(productId, periodIso, billingCycleCount, purchase)
end
Wrapper-->>UI: notify purchase/activation callbacks
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/GUIDE.md (1)
17-21:⚠️ Potential issue | 🟡 MinorStale install coordinate in this guide.
Line 19 still pins
v0.2.1, while README.md advertisesv0.3.0and the intro-pricing APIs demonstrated in §4 only exist post-v0.3.0 ("Unreleased" in CHANGELOG). Readers following this guide on v0.2.1 will hit compile errors ongetIntroPrice,getRecurringPrice,getIntroPeriodIso,getIntroEndMillis, andSubscriptionSpec.withIntro.Bump to the version that ships these APIs (or leave as a templated placeholder the auto-release workflow updates).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/GUIDE.md` around lines 17 - 21, The guide pins an outdated dependency coordinate 'v0.2.1' which lacks the new intro-pricing APIs (getIntroPrice, getRecurringPrice, getIntroPeriodIso, getIntroEndMillis, SubscriptionSpec.withIntro); update the Gradle implementation line to the release that includes these APIs (e.g., change 'v0.2.1' to 'v0.3.0') or replace the version with a templated placeholder (e.g., ${play_billing_wrapper_version}) so the auto-release workflow can inject the correct version.
🧹 Nitpick comments (3)
CHANGELOG.md (1)
3-27: Consider calling out the behavioral nuance ofgetFormattedPrice(id, basePlanId).This section is "Added" only — but integrators who start using
SubscriptionSpec.withIntro(...)will observe an effective behavior change:getFormattedPrice(id, basePlanId)returns the first non-trial phase, which is now the intro price (e.g."$1.00") rather than the recurring price ("$19.00"). Worth a one-line "Changed" bullet pointing togetRecurringPrice(...)as the migration path.📝 Proposed addition
- **`OfferSelector.isIntroEligible(details, basePlanId)`** -- static helper used by the wrapper and exposed for advanced offer routing. + +### Changed + +- **`getFormattedPrice(productId, basePlanId)`** semantics clarified: returns the first + non-trial phase, which is the intro price when an intro offer is selected. Paywall + surfaces that always want the renewal price should migrate to + `getRecurringPrice(productId, basePlanId)`.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CHANGELOG.md` around lines 3 - 27, Add a one-line "Changed" bullet under "Unreleased" explaining that getFormattedPrice(id, basePlanId) now returns the first non-trial phase (which will be the intro price when an intro offer is present) and point integrators to getRecurringPrice(id, basePlanId) as the migration path to obtain the recurring price; reference the functions getFormattedPrice and getRecurringPrice in the sentence so readers can find the APIs easily.library/src/main/java/com/playbillingwrapper/listener/BillingAnalytics.java (1)
10-36: Minor: class-level event list omitsonIntroStarted.The interface Javadoc (lines 17-29) enumerates every callback but was not extended with the new
onIntroStartedentry. Consider adding a bullet for parity withonTrialStarted, since the PR describes them as mutually exclusive.📝 Proposed doc addition
<li>{`@link` `#onTrialStarted`} — a subscription with a free-trial offer was just activated.</li> + <li>{`@link` `#onIntroStarted`} — a subscription with an intro-pricing offer was + just activated (mutually exclusive with {`@link` `#onTrialStarted`}).</li> <li>{`@link` `#onSubscriptionCancelled`} — Play reports an auto-renew true→false transition.</li>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@library/src/main/java/com/playbillingwrapper/listener/BillingAnalytics.java` around lines 10 - 36, The class Javadoc for BillingAnalytics is missing the new onIntroStarted event in the enumerated callback list; update the Javadoc block (the bulleted list under the wrapper invocation) to include a new bullet for {`@link` `#onIntroStarted`} (similar to the existing {`@link` `#onTrialStarted`} entry) describing that it fires when an introductory offer is activated, so the documentation stays consistent with the interface methods.docs/GUIDE.md (1)
259-269: Minor:introPeriodis computed but never used in the example.
getIntroPeriodIso(...)is assigned tointroPeriodat line 262 but the CTA label hard-codes"for 1 month"(line 265). Either use the period in the CTA label or drop the unused local — readers tend to copy/paste these snippets verbatim.📝 Proposed tweak
-String introPeriod = billing.getIntroPeriodIso("com.yourapp.premium", "monthly"); // "P1M" - if (introPrice != null) { - ctaIntro.setText(introPrice + " for 1 month, then " + recurringPrice + " / month"); + String introPeriod = billing.getIntroPeriodIso("com.yourapp.premium", "monthly"); // "P1M" + ctaIntro.setText(introPrice + " for " + prettyIso(introPeriod) + ", then " + recurringPrice + " / month"); } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/GUIDE.md` around lines 259 - 269, The example computes introPeriod via getIntroPeriodIso("com.yourapp.premium", "monthly") but never uses it; update the CTA construction in the ctaIntro.setText call to incorporate introPeriod instead of the hard-coded "for 1 month" when introPrice != null (using introPrice + " for " + introPeriod + ", then " + recurringPrice + " / month"), or remove the introPeriod local entirely if you prefer the simpler example; adjust only the ctaIntro.setText logic and ensure introPeriod and getIntroPeriodIso remain consistent with introPrice and recurringPrice usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Line 28: The changelog header "## v0.3.0 (post-review hardening)" is missing
its release date; update that header (the "## v0.3.0 (post-review hardening)"
line) to include the actual tag date in the same format as sibling entries
(e.g., "## v0.3.0 (post-review hardening) — YYYY-MM-DD") using the real
release/tag date so the entry is uniformly queryable.
In `@library/src/main/java/com/playbillingwrapper/PlayBillingWrapper.java`:
- Around line 1045-1068: The intro-phase metadata (iso and cycles) can mismatch
the intro end computed by getIntroEndMillis(p) because the loop picks the first
SubscriptionSpec with a non-null getIntroPhase rather than the same spec used to
compute introEndMs; to fix, resolve the SubscriptionSpec deterministically the
same way getIntroEndMillis(p) does (or refactor to a helper that returns both
intro end and the matching SubscriptionSpec), then call getIntroPhase(...) for
that same spec to derive iso and billingCycleCount before invoking analytics(a
-> a.onIntroStarted(...)); update either getIntroEndMillis to expose the
matching spec or replace the current for-loop selection with the same selection
logic used by getIntroEndMillis so introEndMs, iso and cycles come from the same
SubscriptionSpec.
- Around line 648-670: getIntroEndMillis currently scans offers itself and can
pick a different offer than getIntroPhase/getIntroPrice which honor
SubscriptionSpec.preferredOfferId; change getIntroEndMillis(PurchaseInfo,
String) to call getIntroPhase(productId, basePlanId) (the same resolution
getIntroPrice/getIntroPhase use) and compute periodMs and cycles from that
returned PricingPhase (or return -1 if null), ensuring the same offer is used;
also update dispatchPurchases to derive ISO period and billingCycleCount from
getIntroPhase(...) rather than re-scanning offers so onIntroStarted telemetry
matches the intro end time; apply the same alignment check to getTrialEndMillis
to avoid the analogous latent mismatch.
In `@README.md`:
- Around line 1037-1039: The documentation update for getFormattedPrice is
missing an explicit behavioral note for integrators: add a "Changed" or
"Behavior" entry to CHANGELOG.md stating that getFormattedPrice(productId,
basePlanId) now returns the intro price when a spec is registered via
withIntro(...) (instead of the recurring price), which may cause paywall labels
to switch from recurring → intro for consumers upgrading from v0.3.0, and
recommend using getRecurringPrice(productId, basePlanId) as the migration target
for the previous recurring value.
---
Outside diff comments:
In `@docs/GUIDE.md`:
- Around line 17-21: The guide pins an outdated dependency coordinate 'v0.2.1'
which lacks the new intro-pricing APIs (getIntroPrice, getRecurringPrice,
getIntroPeriodIso, getIntroEndMillis, SubscriptionSpec.withIntro); update the
Gradle implementation line to the release that includes these APIs (e.g., change
'v0.2.1' to 'v0.3.0') or replace the version with a templated placeholder (e.g.,
${play_billing_wrapper_version}) so the auto-release workflow can inject the
correct version.
---
Nitpick comments:
In `@CHANGELOG.md`:
- Around line 3-27: Add a one-line "Changed" bullet under "Unreleased"
explaining that getFormattedPrice(id, basePlanId) now returns the first
non-trial phase (which will be the intro price when an intro offer is present)
and point integrators to getRecurringPrice(id, basePlanId) as the migration path
to obtain the recurring price; reference the functions getFormattedPrice and
getRecurringPrice in the sentence so readers can find the APIs easily.
In `@docs/GUIDE.md`:
- Around line 259-269: The example computes introPeriod via
getIntroPeriodIso("com.yourapp.premium", "monthly") but never uses it; update
the CTA construction in the ctaIntro.setText call to incorporate introPeriod
instead of the hard-coded "for 1 month" when introPrice != null (using
introPrice + " for " + introPeriod + ", then " + recurringPrice + " / month"),
or remove the introPeriod local entirely if you prefer the simpler example;
adjust only the ctaIntro.setText logic and ensure introPeriod and
getIntroPeriodIso remain consistent with introPrice and recurringPrice usage.
In `@library/src/main/java/com/playbillingwrapper/listener/BillingAnalytics.java`:
- Around line 10-36: The class Javadoc for BillingAnalytics is missing the new
onIntroStarted event in the enumerated callback list; update the Javadoc block
(the bulleted list under the wrapper invocation) to include a new bullet for
{`@link` `#onIntroStarted`} (similar to the existing {`@link` `#onTrialStarted`} entry)
describing that it fires when an introductory offer is activated, so the
documentation stays consistent with the interface methods.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a703eeec-997a-4ae3-acb9-733071fe615d
📒 Files selected for processing (9)
CHANGELOG.mdREADME.mddocs/GUIDE.mdlibrary/src/main/java/com/playbillingwrapper/OfferSelector.javalibrary/src/main/java/com/playbillingwrapper/PlayBillingWrapper.javalibrary/src/main/java/com/playbillingwrapper/listener/BillingAnalytics.javalibrary/src/main/java/com/playbillingwrapper/model/SubscriptionSpec.javalibrary/src/test/java/com/playbillingwrapper/OfferSelectorTest.javalibrary/src/test/java/com/playbillingwrapper/SubscriptionSpecTest.java
…events Post-review fixes for the intro-pricing API surface introduced in 5fb133d. Correctness: - getIntroEndMillis(purchase, basePlanId) now resolves the offer the user purchased via the registered SubscriptionSpec instead of "first named offer with intro phase on the base plan". A preferTrial=true spec that selected a trial-only offer at checkout returns -1 rather than the end of an unrelated intro offer on the same base plan. - getIntroPhase(productId, basePlanId) is now consistent with isIntroEligible: it scans the base plan and prefers the spec's offer (covers combined trial+intro shapes), so paywalls that branch on isIntroEligible always read a non-null phase. - Analytics dispatch for trial / intro events is now independent. A combined offer (free trial -> intro week -> recurring) fires both onTrialStarted and onIntroStarted for the same purchase. Previously the events were mutex-gated, dropping the intro signal on combined offers. Pure-trial / pure-intro offers still fire only their respective event. - Trial event ISO recovery now picks the base plan that actually has a trial phase, not the first registered spec for the product (was incorrect for multi-base-plan products with mixed trial / no-trial plans). API: - OfferSelector.pickOffer / findOfferWithIntroPhase added (package-private) for spec-anchored offer resolution. - OfferSelector.hasIntroPhase elevated to public for advanced offer routing. - isIntroEligible doc tightened: it reflects Play's offer-eligibility filter (first-time-redeemer, audience tags, promo codes), not "has redeemed once". Tests: +5 cases in OfferSelectorTest covering combined trial+intro eligibility, findOfferWithIntroPhase, multi-cycle intro, preferTrial-vs-intro precedence. Docs: README intro section rewritten as a 5-step integration walkthrough; CHANGELOG entries updated to reflect post-fix semantics; GUIDE.md scenario 4.6 added for combined trial+intro offers.
- CHANGELOG: add release date to v0.3.0 heading; new "Changed" entry under Unreleased explaining getFormattedPrice(productId, basePlanId) returns the intro price (not recurring) when SubscriptionSpec.withIntro is registered, and pointing migrators to getRecurringPrice. - BillingAnalytics: add onIntroStarted bullet to the class-level event list with the trial/intro independence note. - docs/GUIDE.md: bump install coordinate from v0.2.1 to v0.3.0; drop unused introPeriod local from §4 paywall snippet (kept inline as a comment so readers still discover the API). - README: same drop in the 5-step intro walkthrough for parity.
Summary
SubscriptionSpec.withIntro,isIntroEligible,getIntroPhase/getIntroPeriodIso/getIntroEndMillis,getIntroPrice/getRecurringPrice,BillingAnalytics.onIntroStarted).getIntroEndMillisandgetIntroPhasetrust the registeredSubscriptionSpecto identify the offer the user purchased; falls back to base-plan scan only when no spec exists.onTrialStartedandonIntroStarted. Pure-trial / pure-intro offers fire only their respective event.Commits
5fb133d— feat: first-class intro pricing ($X first period, then base) APIs0725d69— fix(intro): spec-anchored offer resolution + independent intro/trial eventsTest plan
./gradlew :library:testpasses (BUILD SUCCESSFUL)OfferSelectorTest+13 cases covering eligibility, combined trial+intro, multi-cycle intro, preferTrial-vs-intro precedence, base-plan fallbackSubscriptionSpecTest+2 cases coveringwithIntrohappy path + null offerIdintro_99c_1moshape) — recommended before tagging releaseonIntroStarted; combined offer fires both events; trial-only offer fires onlyonTrialStartedDocs
Summary by CodeRabbit
New Features
Documentation
Tests
Chores