feat: add win-back offers, product status, and JWS promotional offers#66
Conversation
iOS (StoreKit 2): - WinBackOfferInputIOS for iOS 18+ win-back offers - PromotionalOfferJWSInputIOS for WWDC 2025 JWS format (iOS 15+) - introductoryOfferEligibility override option - SubscriptionOfferTypeIOS.WinBack enum value Android (Billing 8.0+): - ProductStatusAndroid enum (OK, NOT_FOUND, NO_OFFERS_AVAILABLE, UNKNOWN) - productStatusAndroid field on ProductAndroid and ProductSubscriptionAndroid Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Regenerate platform-specific types after schema changes in 8023a84. Includes new types for Billing 8.0+ (ProductStatusAndroid, BillingResultAndroid, SubResponseCodeAndroid) and iOS WWDC 2025 APIs (WinBackOfferInputIOS, PromotionalOfferJwsInputIOS, introductoryOfferEligibility). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add winBackOffer support in purchase options (iOS 18+) - Add promotionalOfferJWS for new JWS signature format (WWDC 2025) - Add introductoryOfferEligibility override option (WWDC 2025) - Update purchaseOptions to accept product for win-back offer lookup - Regenerate Types.swift from GQL schema Note: JWS and eligibility override APIs require Xcode 16.4+ to compile. Implementation includes TODOs for when tooling is available. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductStatusAndroid field to product types - Implement getProductStatus() using reflection for 8.0+ compatibility - Maps status codes: OK, NOT_FOUND, NO_OFFERS_AVAILABLE - Gracefully returns null for older billing library versions - Regenerate Types.kt from GQL schema This enables better error handling when products fail to fetch, as 8.0+ now returns status codes instead of silently omitting products. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add release notes for gql 1.3.14, apple 1.3.12, google 1.3.24 - Document ProductStatusAndroid enum in product types page - Document WinBackOfferInputIOS in offer types page - Update llms.txt with new API information for AI assistants Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Google Play Billing Library 8.0 API details (ProductStatus, SubResponseCode) - Add StoreKit 2 WWDC 2025 APIs (win-back offers, JWS promotional offers) - Update Horizon API documentation - Regenerate Claude context file with latest API information Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add documentation checklist to all sync-*.md files - Add example code requirements to audit-code.md - Add local dev testing section to sync-expo-iap.md - Create commit.md skill for structured commit workflows - Improve sync workflow instructions with verification steps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ 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. 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds iOS Win‑Back offers, JWS promotional offers, and introductory‑eligibility flags; Android product status, sub‑response codes, and includeSuspended handling; updates cross‑platform models, GraphQL schema, billing converters/helpers, StoreKit bridge signatures, CLI docs, and extensive documentation/examples sync workflows. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant iOS_Module as "iOS Module"
participant StoreKit_Bridge as "StoreKit Bridge"
participant Product as "Product Catalog"
participant StoreKit as StoreKit
Client->>iOS_Module: requestPurchase(props with winBackOffer)
iOS_Module->>iOS_Module: resolveIosPurchaseProps(props)
iOS_Module->>StoreKit_Bridge: purchaseOptions(iosProps, product)
StoreKit_Bridge->>Product: fetch product.promotionalOffers
alt win-back offer found
Product-->>StoreKit_Bridge: offer details
StoreKit_Bridge->>StoreKit_Bridge: insert winBackOffer option
else not found
StoreKit_Bridge-->>iOS_Module: throw error (offer not found)
end
StoreKit_Bridge-->>iOS_Module: return PurchaseOptions
iOS_Module->>StoreKit: launch purchase with options
StoreKit-->>iOS_Module: purchase result
iOS_Module-->>Client: deliver purchase confirmation/result
sequenceDiagram
participant Client
participant Android_Module as "Android Module"
participant Helpers as Helpers
participant BillingClient as "BillingClient"
participant Converters as Converters
Client->>Android_Module: getAvailablePurchases(options includeSuspendedAndroid)
Android_Module->>Helpers: restorePurchases(includeSuspended = options.includeSuspendedAndroid)
Helpers->>Helpers: build QueryPurchasesParams
alt includeSuspended = true
Helpers->>BillingClient: queryPurchasesAsync(params with setIncludeSuspended via reflection)
else
Helpers->>BillingClient: queryPurchasesAsync(params standard)
end
BillingClient-->>Helpers: purchases list
Helpers->>Converters: toInAppProduct/toSubscriptionProduct(purchase)
Converters->>Converters: ProductDetails.getProductStatus() via reflection (8.x)
Converters-->>Helpers: product models with productStatusAndroid
Helpers-->>Android_Module: returned purchases with status
Android_Module-->>Client: available purchases response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 |
Summary of ChangesHello @hyochan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request expands the in-app purchase capabilities across iOS and Android platforms. It introduces advanced subscription offer types for iOS, such as win-back and JWS-based promotional offers, alongside an option to override introductory offer eligibility. For Android, it enhances product fetching by providing detailed status codes and allows querying suspended subscriptions. Additionally, the PR includes comprehensive updates to internal development workflows and documentation, ensuring better consistency and clarity for future feature development and maintenance. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is an excellent and comprehensive pull request. You've not only added significant new features like Win-Back offers for iOS and Product Status for Android, but you've also done an outstanding job updating all related documentation across the board. The changes to the internal process documents, such as audit-code.md and commit.md, are particularly valuable for maintaining project quality and consistency. The implementation details are solid, with thoughtful considerations for backward compatibility in the Android package and future API availability in the iOS package. The GraphQL schema updates are clear and serve as a strong foundation for these new features. Overall, this is a high-quality contribution that significantly enhances the library's capabilities and maintainability.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
.claude/commands/sync-flutter-iap.md (1)
262-351: Markdownlint violations in new example fences.The added example blocks need blank lines around fences and one fence is missing a language identifier (MD031/MD040). Please adjust to keep lint clean.
.claude/commands/sync-react-native-iap.md (1)
224-302: Address markdownlint MD031/MD040 for the new example fences.The new TSX/MDX fences need blank lines around them, and at least one fence is missing a language tag. This will fail markdownlint.
.claude/commands/sync-kmp-iap.md (1)
236-313: Fix markdownlint MD031/MD040 in the newly added fenced blocks.Please add blank lines around the new fences and specify a language on the fence flagged by MD040 to keep linting green.
🤖 Fix all issues with AI agents
In @.claude/commands/audit-code.md:
- Around line 173-327: The markdown lint errors come from fenced code blocks in
the new examples (e.g., the notes.tsx example, the Swift snippet in
SubscriptionFlowScreen.swift, the Kotlin example in AllProductsScreen.kt and the
MDX examples); fix by ensuring there is a blank line before and after every
triple-backtick fence and that each opening fence includes a language identifier
(e.g., ```typescript, ```swift, ```kotlin, ```mdx) for all added examples and
docs (notes.tsx, API/type MDX blocks, and example snippets) so MD031/MD040 are
satisfied.
In @.claude/commands/sync-expo-iap.md:
- Around line 270-307: Fix markdown fenced-block spacing and nested fences:
ensure there is a blank line before and after every triple-fenced block (e.g.,
the ```tsx blocks under "Example for new iOS feature" and "Example for new
Android feature" and the ```mdx block), and remove any nested triple-fence
sequences inside code examples (replace inner ```typescript in the
"requestSubscription" example with an alternate fence like ~~~ or inline code),
keeping the examples for handleWinBackOffer and products.forEach intact and
properly separated so markdownlint rules MD031/MD040 are satisfied.
- Around line 205-224: Replace the hard-coded, user-specific paths in
LOCAL_OPENIAP_PATHS and the pluginEntries localPath config with portable
placeholders or environment variables: update LOCAL_OPENIAP_PATHS to reference
process.env (e.g., process.env.LOCAL_OPENIAP_IOS / LOCAL_OPENIAP_ANDROID) or
generic relative paths, and ensure the object passed to the app.plugin.js entry
(the localPath field and enableLocalDev flag usage) reads from those
placeholders so no absolute user directory is committed.
In @.claude/commands/sync-godot-iap.md:
- Around line 190-257: Add blank lines before and after every fenced code block
in the document and ensure each fence has a language identifier; specifically,
edit the example fences under "Example Code Guidelines", the iOS example
(gdscript), the Android example (gdscript), and the "Example Documentation
Entry" so each fenced block is preceded and followed by an empty line and the
unlabeled fence in the Documentation Entry is changed to ```gdscript (or
appropriate language) to satisfy markdownlint MD031/MD040; look for the exact
blocks containing RequestSubscriptionIosProps, ProductStatusAndroid match, and
the request_subscription_ios documentation example to locate the fences to
update.
In `@knowledge/_claude-context/context.md`:
- Around line 1493-1503: Update the "Version History" table entries to match
official Google Play Billing Library notes: for Version 8.0 (row with "8.0")
remove "auto-reconnect" and "sub-response codes" and instead describe one-time
product improvements, multiple purchase options/offers, and product-level status
improvements; for Version 8.1 (row with "8.1") revert the detailed features to a
brief "minor release" entry (remove mentions of suspended subscriptions,
includeSuspended, pre-order details, and KEEP_EXISTING) unless you can confirm
those items from an official source; for Version 8.3 (row with "8.3") remove the
"Japan only" restriction from the External Payments program description; keep
the release dates and the "Current Version: 8.3.0" line as-is.
- Around line 2564-2580: Update the table row for originalPlatform to remove the
incorrect "back-deployed to iOS 15" claim (leave it as introduced in iOS 18.4)
or, if you can verify official Apple docs confirm back-deployment, replace the
note with a sourced statement; keep appTransactionID marked as back-deployed to
iOS 15 and ensure the two symbols originalPlatform and appTransactionID are
clearly distinguished in the table.
In `@packages/apple/Sources/Helpers/StoreKitTypesBridge.swift`:
- Around line 410-436: The code silently ignores props.promotionalOfferJWS and
props.introductoryOfferEligibility; update StoreKitTypesBridge.swift to either
apply the new purchase options under a compile-time gate or surface a clear
error: wrap the logic that inserts the options (the options variable where
promotionalOffer and introductoryOfferEligibility would be inserted) in `#if`
swift(>=6.1) and call options.insert(.promotionalOffer(jwsOffer.jws)) and
options.insert(.introductoryOfferEligibility(eligibility)) for
props.promotionalOfferJWS and props.introductoryOfferEligibility respectively;
in the `#else` branch return or throw a PurchaseError.make(...) (matching the
existing pattern used elsewhere in StoreKitTypesBridge.swift) so callers get a
clear unsupported-toolchain error—refer to props.promotionalOfferJWS,
props.introductoryOfferEligibility, options, and mirror the gating/error pattern
used in OpenIapModule.swift and the advancedCommerceData handling.
- Around line 380-408: The code currently uses a compound if-let for
props.winBackOffer and product which silently skips when product is nil; change
this to first guard that if props.winBackOffer (winBackInput) is present but
product is nil then log an error and throw a PurchaseError.make (matching the
pattern used for withOffer) so callers fail fast; otherwise, when product
exists, find the winBackOffer in product.subscription.promotionalOffers, insert
it into options with options.insert(.winBackOffer(offer)) on success or log +
throw PurchaseError.make if the offer isn’t found, and retain the existing log
messages and error codes.
In `@packages/docs/src/pages/docs/updates/notes.tsx`:
- Around line 84-94: The docs list uses wrong enum casing and the example when
expression is non‑exhaustive; update the enum list for ProductStatusAndroid to
use the generated variants Ok, NotFound, NoOffersAvailable, Unknown (references:
ProductStatusAndroid, ProductAndroid, ProductSubscriptionAndroid) and change the
example that evaluates product?.productStatusAndroid (from fetchProducts) to
handle both the null case (product or status is null) and the Unknown case in
the when expression so all branches are covered.
🧹 Nitpick comments (4)
packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt (1)
35-52: Cache reflection lookup and verify Billing 8.0 status codes.
getMethod()is executed per product; consider caching the Method to avoid repeated reflection. Also please confirm the 0/1/2 mappings match the Billing Library 8.0ProductDetails.ProductStatusconstants.♻️ Proposed refactor (cache reflection lookup)
internal object BillingConverters { + private val productStatusMethod by lazy { + runCatching { ProductDetails::class.java.getMethod("getProductStatus") }.getOrNull() + } + /** * Gets the product status from ProductDetails (Billing Library 8.0+). * Returns null for older billing library versions. */ private fun ProductDetails.getProductStatus(): ProductStatusAndroid? { - return runCatching { - // ProductDetails.productStatus is available in Billing Library 8.0+ - val statusMethod = this::class.java.getMethod("getProductStatus") - val status = statusMethod.invoke(this) as? Int + val statusMethod = productStatusMethod ?: return null + return runCatching { + val status = statusMethod.invoke(this) as? Int when (status) { 0 -> ProductStatusAndroid.Ok // ProductDetails.ProductStatus.OK 1 -> ProductStatusAndroid.NotFound // ProductDetails.ProductStatus.NOT_FOUND 2 -> ProductStatusAndroid.NoOffersAvailable // ProductDetails.ProductStatus.NO_OFFERS_AVAILABLE else -> ProductStatusAndroid.Unknown } }.getOrNull() }packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt (1)
49-85: Confirm Billing 8.1setIncludeSuspendedbehavior and fallback expectations.The reflection-based guard is reasonable, but please confirm from Billing 8.1 docs that
setIncludeSuspended(true)is valid only for SUBS and that silently ignoring it on older libs is the intended behavior. If you want stronger observability, consider logging when the method isn’t available.packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt (2)
611-650: Prefer tolerant parsing forProductStatusAndroid.
Since anUnknownenum value exists, falling back to it avoids crashes if Play adds new status strings in the future.♻️ Proposed change
- else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + else -> ProductStatusAndroid.UnknownBased on learnings, update the generator rather than hand-editing this file.
3530-3587: Guard against conflicting offer inputs.
Consider validating thatpromotionalOfferJWSand legacywithOfferaren’t both set, and that win‑back/intro eligibility are only used for subscriptions, to avoid invalid StoreKit option combinations.
- Fix markdown lint issues (MD031/MD040) in audit-code.md - Fix markdown fenced-block spacing in sync-expo-iap.md, sync-godot-iap.md - Replace hard-coded paths with placeholders in sync-expo-iap.md - Update Google Play Billing version history (correct 8.0, 8.1, 8.3 descriptions) - Fix originalPlatform back-deployment claim in context.md - Add guard for winBackOffer product context in StoreKitTypesBridge.swift - Use #if swift(>=6.1) for JWS promo offers and intro eligibility override - Fix ProductStatusAndroid enum casing (Ok, NotFound, etc.) in notes.tsx - Add missing Unknown and null cases to when expression Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
knowledge/_claude-context/context.md (1)
1511-1532: Correct the OpenIAP Note — auto-reconnection is opt-in, not always enabled.The first claim is accurate:
enableAutoServiceReconnection()was introduced in Google Play Billing Library 8.0.0. However, the OpenIAP Note is misleading. According to official documentation, auto-reconnection is opt-in and requires an explicit call toenableAutoServiceReconnection()on the BillingClient builder—it is not enabled by default. Update the note to clarify that developers (or OpenIAP's integration) must explicitly enable it if they want auto-reconnect behavior; claiming "no configuration needed" contradicts the official API design.
🤖 Fix all issues with AI agents
In `@knowledge/_claude-context/context.md`:
- Around line 1493-1499: Rename the duplicate section heading "## Version
History" to a unique title (for example "## Google Play Billing Version
History") to resolve the MD024 duplicate-heading lint error; update the heading
text in the knowledge/_claude-context/context.md file where the "## Version
History" heading appears so any internal links or references that point to that
header are adjusted accordingly.
- Around line 2765-2768: Split the combined availability note into two clear
annotations: mark appTransactionID with "New in iOS 18.4 (back-deployed to iOS
15)" and mark originalPlatform with "New in iOS 18.4 (iOS 18.4+ only)" so the
comment for appTransaction.appTransactionID and appTransaction.originalPlatform
accurately reflect that only appTransactionID is back‑deployed.
In `@packages/apple/Sources/Helpers/StoreKitTypesBridge.swift`:
- Around line 380-417: The code silently ignores props.winBackOffer on
unsupported OS versions; update the `#available` block by adding an else branch
that logs and throws a PurchaseError when props.winBackOffer is non-nil but the
platform is unsupported. Specifically, check props.winBackOffer outside or at
the top of the availability check and if the platform is unsupported call
OpenIapLog.error (include the offerId from props.winBackOffer) and throw
PurchaseError.make (use code: .developerError and productId: props.sku) so
callers fail fast instead of proceeding to purchase without the win-back offer;
keep the existing handling inside the available block (product.subscription,
finding promotionalOffers, options.insert(.winBackOffer(offer))) unchanged.
🧹 Nitpick comments (2)
packages/apple/Sources/Helpers/StoreKitTypesBridge.swift (1)
362-362: Rename this iOS-specific helper to include theIOSsuffix.
purchaseOptionsis Apple/iOS-specific but doesn’t follow the...IOSnaming rule forpackages/apple/Sources/**/*.swift. Consider renaming topurchaseOptionsIOSand updating call sites for consistency. As per coding guidelines, ....claude/commands/sync-godot-iap.md (1)
91-97: Prefer placeholders for version examples to prevent drift.This block is meant as a template; hard-coded versions will go stale quickly. Consider using
x.y.zplaceholders to keep it evergreen.📝 Suggested update
-{ - "gql": "1.3.11", - "apple": "1.3.9", - "google": "1.3.21" -} +{ + "gql": "x.y.z", + "apple": "x.y.z", + "google": "x.y.z" +}
- Add reply with commit link before resolving fixed threads - Add reply templates for different fix scenarios - Update decision tree with reply requirements - Add Thread Resolution Rules table - Clarify that invalid reviews get replies but NOT resolved - Add important notes about never silent resolving Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Project-specific review-pr.md now only contains: - Project-specific build commands table - Project conventions reference - Links to CLAUDE.md and knowledge/internal/ Global command at ~/.claude/commands/review-pr.md handles: - Full workflow documentation - GraphQL API calls - Decision tree - Thread resolution rules - Reply templates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename "Version History" to "Google Play Billing Version History" (MD024) - Clarify appTransactionID vs originalPlatform back-deployment in context.md - Add else branch to fail fast when winBackOffer used on unsupported OS Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Changes
GraphQL Schema (packages/gql)
WinBackOfferInputIOS- Win-back offer input typeProductStatusAndroid- Product fetch status enumPromotionalOfferJWSInputIOS- JWS format promotional offersSubscriptionOfferTypeIOS.WinBack- New offer type enum valueiOS (packages/apple)
Android (packages/google)
Documentation (packages/docs)
Skills & Knowledge
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.