Skip to content

feat(google): support one-time purchase discount offers#51

Merged
hyochan merged 3 commits into
mainfrom
feat/android-offer-details-list
Dec 10, 2025
Merged

feat(google): support one-time purchase discount offers#51
hyochan merged 3 commits into
mainfrom
feat/android-offer-details-list

Conversation

@hyochan
Copy link
Copy Markdown
Member

@hyochan hyochan commented Dec 10, 2025

BREAKING CHANGE: oneTimePurchaseOfferDetailsAndroid type changed from
single object to array to support multiple discount offers.

Migration:

  • Before: product.oneTimePurchaseOfferDetailsAndroid?.formattedPrice
  • After: product.oneTimePurchaseOfferDetailsAndroid?.firstOrNull()?.formattedPrice

Changes:

  • Add discount-related types: DiscountDisplayInfoAndroid, DiscountAmountAndroid,
    ValidTimeWindowAndroid, LimitedQuantityInfoAndroid, PreorderDetailsAndroid,
    RentalDetailsAndroid
  • Update BillingConverters to use oneTimePurchaseOfferDetailsList API
    (Google Play Billing Library 7.0+)
  • Add discount display support in Example app (ProductCard, Modals)
  • Add discount feature documentation
  • Fix verifyPurchaseWithIapkit return type usage in Example screens

Related issue: hyochan/react-native-iap#3102

Summary by CodeRabbit

  • New Features

    • Android: Added discount support for one-time purchases (percentage/amount), multiple offers per product, validity windows, limited-quantity and rental metadata.
    • UI: Shows original price when discounted, highlights discounted price and displays discount badges and per-offer details.
  • Documentation

    • New comprehensive "Discounts (Android)" guide with multi-language examples, UI patterns, and best practices.

✏️ Tip: You can customize this high-level summary in your review settings.

BREAKING CHANGE: `oneTimePurchaseOfferDetailsAndroid` type changed from
single object to array to support multiple discount offers.

Migration:
- Before: `product.oneTimePurchaseOfferDetailsAndroid?.formattedPrice`
- After: `product.oneTimePurchaseOfferDetailsAndroid?.firstOrNull()?.formattedPrice`

Changes:
- Add discount-related types: DiscountDisplayInfoAndroid, DiscountAmountAndroid,
  ValidTimeWindowAndroid, LimitedQuantityInfoAndroid, PreorderDetailsAndroid,
  RentalDetailsAndroid
- Update BillingConverters to use oneTimePurchaseOfferDetailsList API
  (Google Play Billing Library 7.0+)
- Add discount display support in Example app (ProductCard, Modals)
- Add discount feature documentation
- Fix verifyPurchaseWithIapkit return type usage in Example screens

Requires: Google Play Billing Library 7.0+
@hyochan hyochan added the 🎯 feature New feature label Dec 10, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 10, 2025

Walkthrough

This PR adds Android-specific discount, rental, limited-quantity, and validity models and changes one-time purchase offer details from a single object to an array across product and subscription types. Converters, GraphQL schema, docs, example UI, and serialization/parsing were updated to support multiple enriched offers.

Changes

Cohort / File(s) Summary
Core type definitions (Kotlin / Swift)
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt, packages/apple/Sources/Models/Types.swift
Added Android-specific types: DiscountAmountAndroid, DiscountDisplayInfoAndroid, LimitedQuantityInfoAndroid, RentalDetailsAndroid, ValidTimeWindowAndroid. Introduced ProductAndroidOneTimePurchaseOfferDetail and changed oneTimePurchaseOfferDetailsAndroid from a single object to List<ProductAndroidOneTimePurchaseOfferDetail>? on ProductAndroid and ProductSubscriptionAndroid.
GraphQL schema
packages/gql/src/type-android.graphql
Added GraphQL types for RentalDetailsAndroid, ValidTimeWindowAndroid, LimitedQuantityInfoAndroid, DiscountAmountAndroid, DiscountDisplayInfoAndroid, and ProductAndroidOneTimePurchaseOfferDetail. Changed oneTimePurchaseOfferDetailsAndroid to a non-nullable list [ProductAndroidOneTimePurchaseOfferDetail!] in ProductAndroid and ProductSubscriptionAndroid.
Billing converters (Play & Horizon)
packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt, packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt
Convert platform offer details into the new ProductAndroidOneTimePurchaseOfferDetail shape; aggregate legacy single-offer info into single-element lists when needed. Added helper toOfferDetail() (Play) and adjusted Horizon mappings to populate list-based oneTimePurchaseOfferDetailsAndroid, including discount, valid time window, limited quantity, rental, and preorder fields.
Example app UI
packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt, packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/ProductCard.kt
Updated UI to iterate over multiple one-time offers: render one card per offer with offer metadata (price, offerId, token, tags, discount badge, validity window, limited quantity, preorder, rental). ProductCard renders original price (strikethrough) and discount badge when discountDisplayInfo present and adjusts price color.
Example app verification flows
packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt, packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
Simplified verification result handling: replaced firstOrNull()?.isValid pattern with direct result.isValid (single-result expectation).
Documentation & site pages
packages/docs/src/pages/docs.tsx, packages/docs/src/pages/docs/features/discount.tsx, packages/docs/src/pages/docs/types.tsx, packages/docs/src/pages/docs/updates/notes.tsx
Added new "Discounts (Android)" feature page with multi-language samples and guidance; updated Types page to document oneTimePurchaseOfferDetailsAndroid as an array and new fields (offerToken, discountDisplayInfo, fullPriceMicros, validTimeWindow, limitedQuantityInfo, rentalDetailsAndroid); updated notes to read preorder details via the first element of the offers array.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing focused review:
    • Converters: Play and Horizon mapping correctness and fallbacks (empty lists vs legacy single field).
    • Serialization/parsing: JSON (de)serialization of new Kotlin/Swift types and list handling.
    • UI: iteration, null-safety for empty offer lists, and visual correctness of discount badges.
    • GraphQL schema: ensure generated types and clients align with new list-shaped fields.

Possibly related PRs

Suggested labels

🤖 android, ⬡ gql

Poem

🐰 Hop-hop — offers now abound,

Arrays of sales all gathered round.
Discounts, rentals, time windows shine,
Tokens, tags, and micros align.
I nibble code and dance — hooray, Android's fine! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.82% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for one-time purchase discount offers on Android, which aligns with the primary objectives of the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/android-offer-details-list

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt (1)

1270-1286: IAPKit result handling is correct; consider redacting token in logs

Returning result.isValid directly from verifyPurchaseWithIapkit matches the helper’s contract and simplifies the call. The only tweak I’d suggest is to align with PurchaseFlowScreen by logging a truncated purchaseToken (e.g., token.take(6)) instead of the full value, to avoid leaking full tokens in logs even in example code.

packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt (1)

560-577: Direct use of result.isValid looks good; watch for blocking network work

The switch to val result = verifyPurchaseWithIapkit(...); return result.isValid is correct and clearer than handling a collection of results. Since verifyPurchaseWithIapkit is a synchronous HTTP call, consider wrapping it in withContext(Dispatchers.IO) inside this suspend helper (or updating the util) so that LaunchedEffect on the main dispatcher doesn’t end up doing network I/O on the UI thread.

packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/ProductCard.kt (1)

114-158: Discount UI is solid; consider safer handling for malformed fullPriceMicros

Using the first one-time offer and discountDisplayInfo to drive the strikethrough price and badge is well-structured and null-safe. One small improvement: instead of defaulting to 0L when fullPriceMicros?.toLongOrNull() fails, you could skip rendering the original-price line in that case to avoid ever showing a bogus “0.00” crossed-out price.

packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt (1)

139-216: Multi-offer rendering correctly reflects new one-time offer model

The new block that handles oneTimePurchaseOfferDetailsAndroid as a list is well-structured: it safely iterates offers, surfaces discount, time-window, limited-quantity, preorder, and rental metadata, and uses visual cues (card tint) when discounts are present. As a future enhancement, you might format the millis fields (valid window, preorder times) into human-readable dates, but as a diagnostics-focused modal the current raw values are acceptable.

packages/docs/src/pages/docs/features/discount.tsx (1)

268-369: Minor: prefer formatted price source over currency code math in UI example

In ProductCard, the original price is reconstructed from micros and shown as {currency} {fullPrice.toFixed(2)}, where currency is a currency code (e.g., USD). For clearer UX and localization, consider either:

  • Using a formatted price string from Billing (if available), or
  • Formatting via Intl.NumberFormat using the currency code.

Also, in the earlier fetch example you always log discount?.percentageDiscount + '% OFF'; you might mirror the logic from getDiscountText() (handling discountAmount or falling back to SALE) to avoid logging null% OFF.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c05f2fe and 7c73363.

⛔ Files ignored due to path filters (4)
  • packages/gql/src/generated/Types.kt is excluded by !**/generated/**
  • packages/gql/src/generated/Types.swift is excluded by !**/generated/**
  • packages/gql/src/generated/types.dart is excluded by !**/generated/**
  • packages/gql/src/generated/types.ts is excluded by !**/generated/**
📒 Files selected for processing (13)
  • packages/apple/Sources/Models/Types.swift (6 hunks)
  • packages/docs/src/pages/docs.tsx (3 hunks)
  • packages/docs/src/pages/docs/features/discount.tsx (1 hunks)
  • packages/docs/src/pages/docs/types.tsx (1 hunks)
  • packages/docs/src/pages/docs/updates/notes.tsx (1 hunks)
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt (1 hunks)
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt (1 hunks)
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt (1 hunks)
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/ProductCard.kt (1 hunks)
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt (4 hunks)
  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt (11 hunks)
  • packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt (5 hunks)
  • packages/gql/src/type-android.graphql (3 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: iOS-specific functions must end with IOS suffix (e.g., clearTransactionIOS, getAppTransactionIOS)
Android-specific functions must end with Android suffix (e.g., acknowledgePurchaseAndroid, consumePurchaseAndroid)
Use action prefixes for function naming: get for retrieval, request for async operations, clear for removal, is/has for boolean checks, show/present for UI display, begin/finish/end for process control

Prefer interface for defining object shapes in TypeScript

Files:

  • packages/docs/src/pages/docs/types.tsx
  • packages/docs/src/pages/docs/features/discount.tsx
  • packages/docs/src/pages/docs/updates/notes.tsx
  • packages/docs/src/pages/docs.tsx
packages/docs/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

packages/docs/**/*.{ts,tsx}: Use kebab-case for search modal IDs (e.g., id: 'request-products')
Modal state should be defined once at the app root level using Preact Signals (signal from '@preact/signals-react'), not instantiated multiple times
ALL components must fit within parent boundaries and never overflow outside parent containers; use overflow-hidden, break-words, and whitespace-nowrap as needed
Delete unused components, functions, and imports immediately; do not keep commented-out code or unused variables
ANY function that returns a Promise must be wrapped with void operator when used where a void return is expected (e.g., event handlers)

Files:

  • packages/docs/src/pages/docs/types.tsx
  • packages/docs/src/pages/docs/features/discount.tsx
  • packages/docs/src/pages/docs/updates/notes.tsx
  • packages/docs/src/pages/docs.tsx
packages/docs/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

packages/docs/**/*: Before committing, run npx prettier --write, npm run lint, bun run tsc or npm run typecheck, and npm run build to verify formatting, linting, types, and build success
Use conventional commit format with lowercase type prefix and lowercase description (e.g., feat: add user authentication)

Files:

  • packages/docs/src/pages/docs/types.tsx
  • packages/docs/src/pages/docs/features/discount.tsx
  • packages/docs/src/pages/docs/updates/notes.tsx
  • packages/docs/src/pages/docs.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{ts,tsx,js,jsx}: Use camelCase for variable and function names
Use PascalCase for class and component names
Always use async/await for handling promises instead of .then() chains
Add JSDoc comments for public functions and exported APIs
Use const by default, let if reassignment is needed, avoid var

Files:

  • packages/docs/src/pages/docs/types.tsx
  • packages/docs/src/pages/docs/features/discount.tsx
  • packages/docs/src/pages/docs/updates/notes.tsx
  • packages/docs/src/pages/docs.tsx
packages/google/**/*.kt

📄 CodeRabbit inference engine (CLAUDE.md)

DO NOT add Android suffix to function names in the Android-only package, even for Android-specific APIs (e.g., use acknowledgePurchase() not acknowledgePurchaseAndroid())

Files:

  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/ProductCard.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
packages/gql/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Run bun run generate to regenerate types for all platforms (TypeScript, Swift, Kotlin, Dart) from the GraphQL schema

Files:

  • packages/gql/src/type-android.graphql
packages/apple/**/*.swift

📄 CodeRabbit inference engine (CLAUDE.md)

packages/apple/**/*.swift: iOS-specific functions MUST have IOS suffix (e.g., presentCodeRedemptionSheetIOS(), showManageSubscriptionsIOS())
Use Pascal case for acronyms at the beginning or middle of names (e.g., IapManager, IapPurchase), but ALL CAPS only when appearing as a suffix (e.g., ProductIAP, OpenIAP)

Files:

  • packages/apple/Sources/Models/Types.swift
packages/apple/Sources/Models/Types.swift

📄 CodeRabbit inference engine (CLAUDE.md)

DO NOT edit Types.swift in Sources/Models/ as it is auto-generated from the OpenIAP GraphQL schema; regenerate using ./scripts/generate-types.sh

Files:

  • packages/apple/Sources/Models/Types.swift
packages/apple/Sources/Models/**/*.swift

📄 CodeRabbit inference engine (CLAUDE.md)

OpenIAP official types in Sources/Models/ must match types defined at openiap.dev/docs/types

Files:

  • packages/apple/Sources/Models/Types.swift
🧠 Learnings (8)
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to **/*.{ts,tsx} : Android-specific functions must end with `Android` suffix (e.g., `acknowledgePurchaseAndroid`, `consumePurchaseAndroid`)

Applied to files:

  • packages/docs/src/pages/docs/types.tsx
  • packages/docs/src/pages/docs/updates/notes.tsx
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/google/**/*.kt : DO NOT add `Android` suffix to function names in the Android-only package, even for Android-specific APIs (e.g., use `acknowledgePurchase()` not `acknowledgePurchaseAndroid()`)

Applied to files:

  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/ProductCard.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/google/openiap/src/main/Types.kt : DO NOT edit `openiap/src/main/Types.kt` as it is auto-generated from the GraphQL schema; regenerate it using `./scripts/generate-types.sh`

Applied to files:

  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
  • packages/gql/src/type-android.graphql
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/google/openiap/src/main/java/dev/hyo/openiap/utils/**/*.kt : Place reusable Kotlin helper functions in `openiap/src/main/java/dev/hyo/openiap/utils/`

Applied to files:

  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/google/openiap-versions.json : To update OpenIAP GraphQL types in the Android library, edit the `gql` field in `openiap-versions.json`, then run `./scripts/generate-types.sh` and compile to verify

Applied to files:

  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/gql/src/type-android.graphql
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/apple/Sources/Models/**/*.swift : OpenIAP official types in `Sources/Models/` must match types defined at [openiap.dev/docs/types](https://www.openiap.dev/docs/types)

Applied to files:

  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/apple/Sources/Models/Types.swift
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/apple/openiap-versions.json : To update OpenIAP GraphQL types in the iOS/macOS library, edit the `gql` field in `openiap-versions.json`, run `./scripts/generate-types.sh`, and run `swift test` to verify compatibility

Applied to files:

  • packages/gql/src/type-android.graphql
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/apple/Sources/Models/Types.swift : DO NOT edit `Types.swift` in `Sources/Models/` as it is auto-generated from the OpenIAP GraphQL schema; regenerate using `./scripts/generate-types.sh`

Applied to files:

  • packages/gql/src/type-android.graphql
  • packages/apple/Sources/Models/Types.swift
🧬 Code graph analysis (5)
packages/docs/src/pages/docs/features/discount.tsx (1)
packages/docs/src/hooks/useScrollToHash.ts (1)
  • useScrollToHash (4-28)
packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt (2)
packages/apple/Sources/OpenIapModule.swift (1)
  • verifyPurchaseWithIapkit (648-722)
packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt (1)
  • verifyPurchaseWithIapkit (83-167)
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt (1)
packages/gql/src/generated/types.ts (6)
  • DiscountAmountAndroid (79-84)
  • DiscountDisplayInfoAndroid (90-101)
  • LimitedQuantityInfoAndroid (229-234)
  • ProductAndroidOneTimePurchaseOfferDetail (408-439)
  • RentalDetailsAndroid (788-796)
  • ValidTimeWindowAndroid (975-980)
packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt (1)
packages/gql/src/generated/types.ts (7)
  • DiscountDisplayInfoAndroid (90-101)
  • DiscountAmountAndroid (79-84)
  • ValidTimeWindowAndroid (975-980)
  • LimitedQuantityInfoAndroid (229-234)
  • PreorderDetailsAndroid (356-367)
  • RentalDetailsAndroid (788-796)
  • ProductAndroidOneTimePurchaseOfferDetail (408-439)
packages/apple/Sources/Models/Types.swift (1)
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt (7)
  • discountAmountMicros (677-702)
  • discountAmount (708-735)
  • maximumQuantity (917-942)
  • discountDisplayInfo (1090-1169)
  • offerToken (2161-2184)
  • rentalExpirationPeriod (1767-1793)
  • endTimeMillis (1971-1996)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Android
🔇 Additional comments (14)
packages/docs/src/pages/docs.tsx (1)

13-13: Discount docs page wiring matches existing routing pattern

Importing Discount and adding both the sidebar NavLink and features/discount route are consistent with the existing feature pages (purchase, subscription, etc.). Paths and labels line up, so navigation to the new Discounts (Android) docs should work as expected.

Also applies to: 174-182, 258-258

packages/docs/src/pages/docs/updates/notes.tsx (1)

95-100: Kotlin snippet correctly reflects array-based one-time offer details

Updating the example to treat oneTimePurchaseOfferDetailsAndroid as an array and using .firstOrNull()?.preorderDetailsAndroid matches the new list-based API and keeps the sample idiomatic and null-safe.

packages/docs/src/pages/docs/types.tsx (1)

367-389: Android one-time offer docs now aligned with new type shape

The updated description correctly documents the field as an array and enumerates the new offer metadata fields, linking to the dedicated Discounts page and calling out the Billing Library 7.0+ requirement. This matches the generated types and Kotlin models.

packages/docs/src/pages/docs/features/discount.tsx (1)

7-777: Comprehensive Discounts page is well-structured and consistent with new Android types

The page ties together data structures, fetching, UI patterns, time/quantity logic, and purchase flow in a way that matches the new one-time offer types and Kotlin/Swift/TS models. The sectioning, anchors, and useScrollToHash/SEO match existing docs conventions.

packages/apple/Sources/Models/Types.swift (1)

327-346: Android discount offer Swift types look consistent; confirm they’re generator-synced

The new Android structs (DiscountAmountAndroid, DiscountDisplayInfoAndroid, LimitedQuantityInfoAndroid, RentalDetailsAndroid, ValidTimeWindowAndroid) and the expanded ProductAndroidOneTimePurchaseOfferDetail (now used as an array on both ProductAndroid and ProductSubscriptionAndroid) line up with the documented TS/GQL shapes and the docs updates for discount offers.

Given this file is auto-generated, please ensure these changes come from regenerating Types via the GraphQL schema (./scripts/generate-types.sh / npm run generate), not from manual edits, so Swift, Kotlin, and TS types all stay in lockstep with the docs.

Based on learnings, this file should only change via the generator.

Also applies to: 399-407, 432-448, 450-477, 505-513, 665-673, 724-731

packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt (1)

26-55: Horizon converters correctly adapt to list-based one-time offers

The conversions now wrap Horizon’s single oneTimePurchaseOfferDetails into a singleton ProductAndroidOneTimePurchaseOfferDetail list for both in-app and subscription products, leaving all discount/time/quantity/preorder/rental fields null (as Horizon doesn’t expose them) and wiring into the new oneTimePurchaseOfferDetailsAndroid list fields.

Using an empty string for offerToken is reasonable for Horizon as long as downstream code treats Horizon offers as non-purchasable via offer tokens and only uses tokens for Google Play.

Also applies to: 90-119

packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt (1)

4-24: All accessed API properties are available in BillingClient 8.x

The new toOfferDetail() extension correctly maps ProductDetails.OneTimePurchaseOfferDetails into ProductAndroidOneTimePurchaseOfferDetail, including all accessed properties: discountDisplayInfo, validTimeWindow, limitedQuantityInfo, preorderDetails, rentalDetails, fullPriceMicros, offerId, offerToken, and offerTags. The use of oneTimePurchaseOfferDetailsList on ProductDetails with defensive runCatching is appropriate. Both toInAppProduct() and toSubscriptionProduct() correctly use this list while maintaining backward compatibility with the legacy single offer.

packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt (7)

673-735: New discount-related Android types are consistent with the GraphQL schema.

DiscountAmountAndroid and DiscountDisplayInfoAndroid correctly mirror their TypeScript counterparts in packages/gql/src/generated/types.ts. The fromJson/toJson methods follow the established patterns in this auto-generated file.


913-942: LimitedQuantityInfoAndroid matches the schema definition.

Fields correctly map TypeScript number to Kotlin Int with proper Number.toInt() conversions in fromJson.


1036-1084: One-time purchase offers now correctly support multiple offers as a list.

The change from a single object to List<ProductAndroidOneTimePurchaseOfferDetail>? aligns with the PR objective to support multiple discount offers. The nullable list handling follows the same pattern used for subscriptionOfferDetailsAndroid.


1086-1169: ProductAndroidOneTimePurchaseOfferDetail fully matches the GraphQL schema.

All 12 fields align with the TypeScript definition in packages/gql/src/generated/types.ts (lines 407-438). The fromJson/toJson methods correctly handle nested objects (DiscountDisplayInfoAndroid, LimitedQuantityInfoAndroid, RentalDetailsAndroid, ValidTimeWindowAndroid) and the list of offerTags.


1239-1287: ProductSubscriptionAndroid consistently updated to support one-time purchase offer lists.

The same list-based pattern is applied here as in ProductAndroid, maintaining consistency across product types.


1763-1793: RentalDetailsAndroid and ValidTimeWindowAndroid correctly implement their schema definitions.

Both new data classes match their TypeScript counterparts in field names, types, and nullability. The ISO 8601 period format for rental details and millisecond epoch timestamps for valid time windows are appropriately typed as String to preserve precision.

Also applies to: 1967-1996


1-7: Auto-generated file verified against GraphQL schema.

Based on learnings, this file is auto-generated and should not be edited directly. All changes correctly reflect the updated GraphQL schema for one-time purchase discount offers. The new types (DiscountAmountAndroid, DiscountDisplayInfoAndroid, LimitedQuantityInfoAndroid, RentalDetailsAndroid, ValidTimeWindowAndroid) and the conversion of oneTimePurchaseOfferDetailsAndroid to a list are consistent with the TypeScript definitions in packages/gql/src/generated/types.ts.

Comment thread packages/docs/src/pages/docs/features/discount.tsx
Comment thread packages/gql/src/type-android.graphql
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/docs/src/pages/docs/features/discount.tsx (1)

642-694: Verify requestPurchase type discriminator value in TS & Kotlin examples

The requestPurchase examples now correctly include the required type discriminator, but they use the literal 'inapp' / "inapp". Previous review feedback referenced type: 'in-app' for in‑app purchases, so there may still be a mismatch between these examples and the actual RequestPurchaseProps.type union.

Please double‑check the canonical value in your shared types (e.g., RequestPurchaseProps / RequestPurchaseAndroidProps) and ensure:

  • The TypeScript example’s type value matches the allowed union.
  • The Kotlin example passes the same canonical string into RequestPurchaseProps.

If the union expects 'in-app', both examples should be updated accordingly; if 'inapp' is now the official value, you’re good and the earlier guidance can be ignored.

You can verify consistency with a quick scan of the codebase:

#!/bin/bash
# Inspect RequestPurchase-related types and usages of the type discriminator

# Find the RequestPurchaseProps definition
rg "interface RequestPurchaseProps|type RequestPurchaseProps" -n packages -S

# Check what string literals are used for the discriminator
rg "type\s*[:=]\s*['\"]in-app['\"]|['\"]inapp['\"]" -n packages -S
🧹 Nitpick comments (1)
packages/docs/src/pages/docs/features/discount.tsx (1)

7-18: Add JSDoc for exported Discount page component

Discount is the default export and part of the public docs surface, but it lacks a JSDoc block. Add a brief JSDoc describing the page’s purpose to align with the guideline that public functions/exports should be documented.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c73363 and e6b308d.

📒 Files selected for processing (1)
  • packages/docs/src/pages/docs/features/discount.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: iOS-specific functions must end with IOS suffix (e.g., clearTransactionIOS, getAppTransactionIOS)
Android-specific functions must end with Android suffix (e.g., acknowledgePurchaseAndroid, consumePurchaseAndroid)
Use action prefixes for function naming: get for retrieval, request for async operations, clear for removal, is/has for boolean checks, show/present for UI display, begin/finish/end for process control

Prefer interface for defining object shapes in TypeScript

Files:

  • packages/docs/src/pages/docs/features/discount.tsx
packages/docs/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

packages/docs/**/*.{ts,tsx}: Use kebab-case for search modal IDs (e.g., id: 'request-products')
Modal state should be defined once at the app root level using Preact Signals (signal from '@preact/signals-react'), not instantiated multiple times
ALL components must fit within parent boundaries and never overflow outside parent containers; use overflow-hidden, break-words, and whitespace-nowrap as needed
Delete unused components, functions, and imports immediately; do not keep commented-out code or unused variables
ANY function that returns a Promise must be wrapped with void operator when used where a void return is expected (e.g., event handlers)

Files:

  • packages/docs/src/pages/docs/features/discount.tsx
packages/docs/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

packages/docs/**/*: Before committing, run npx prettier --write, npm run lint, bun run tsc or npm run typecheck, and npm run build to verify formatting, linting, types, and build success
Use conventional commit format with lowercase type prefix and lowercase description (e.g., feat: add user authentication)

Files:

  • packages/docs/src/pages/docs/features/discount.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{ts,tsx,js,jsx}: Use camelCase for variable and function names
Use PascalCase for class and component names
Always use async/await for handling promises instead of .then() chains
Add JSDoc comments for public functions and exported APIs
Use const by default, let if reassignment is needed, avoid var

Files:

  • packages/docs/src/pages/docs/features/discount.tsx
🧠 Learnings (3)
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to **/*.{ts,tsx} : Android-specific functions must end with `Android` suffix (e.g., `acknowledgePurchaseAndroid`, `consumePurchaseAndroid`)

Applied to files:

  • packages/docs/src/pages/docs/features/discount.tsx
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/apple/Sources/Models/**/*.swift : OpenIAP official types in `Sources/Models/` must match types defined at [openiap.dev/docs/types](https://www.openiap.dev/docs/types)

Applied to files:

  • packages/docs/src/pages/docs/features/discount.tsx
📚 Learning: 2025-12-06T20:15:59.223Z
Learnt from: CR
Repo: hyodotdev/openiap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T20:15:59.223Z
Learning: Applies to packages/google/**/*.kt : DO NOT add `Android` suffix to function names in the Android-only package, even for Android-specific APIs (e.g., use `acknowledgePurchase()` not `acknowledgePurchaseAndroid()`)

Applied to files:

  • packages/docs/src/pages/docs/features/discount.tsx
🧬 Code graph analysis (1)
packages/docs/src/pages/docs/features/discount.tsx (1)
packages/docs/src/hooks/useScrollToHash.ts (1)
  • useScrollToHash (4-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Android

@hyochan hyochan merged commit 0ad4747 into main Dec 10, 2025
5 checks passed
@hyochan hyochan deleted the feat/android-offer-details-list branch December 10, 2025 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎯 feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant