fix(types): add missing discountsIOS and other platform-specific fields#3046
fix(types): add missing discountsIOS and other platform-specific fields#3046
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds Android one-time-offer details and product name to Nitro payloads, expands Nitro types and purchase mappings with many iOS-prefixed fields plus Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Screen as AvailablePurchases
participant Ctx as DataModalProvider
participant Modal as DataModal
User->>Screen: tap purchase item
Screen->>Ctx: showData(purchase, "Purchase: <id>")
Ctx->>Modal: set visible, title, payload (redact purchaseToken)
Modal-->>User: display JSON
User->>Modal: Copy / Close
Modal->>Ctx: hideModal()
Ctx->>Modal: clear data after delay
sequenceDiagram
autonumber
actor App as PurchaseFlowContainer
participant IAP as react-native-iap
participant UI as PurchaseFlow
App->>IAP: connect()
App->>IAP: getStorefront()
IAP-->>App: storefront | error
App->>UI: provide {storefront, isFetchingStorefront, onRefreshStorefront}
User->>UI: request refresh
UI->>App: onRefreshStorefront()
App->>IAP: getStorefront()
IAP-->>App: storefront
App-->>UI: update props
sequenceDiagram
autonumber
actor User
participant Sub as SubscriptionFlow
participant IAP as react-native-iap
participant Store as PlatformStore
User->>Sub: choose plan change
alt Android path
Sub->>IAP: requestPurchase(sku, {offerToken, replacementMode, purchaseToken})
else iOS path
Sub->>IAP: requestSubscription(sku, options)
end
IAP-->>Sub: purchase result
Sub->>IAP: getAvailablePurchases() / getSubscriptions()
IAP-->>Sub: updated caches
Sub-->>User: show updated plan state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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 significantly enhances the library's capability to handle platform-specific in-app purchase and product details by adding numerous missing fields for both iOS and Android. It also brings substantial improvements to type safety by introducing structured types and eliminating 'any' assertions. Furthermore, the example applications have been refactored to use a new global data display modal and feature more sophisticated Android subscription management, making them more robust and user-friendly. Highlights
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
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3046 +/- ##
==========================================
+ Coverage 68.39% 68.97% +0.58%
==========================================
Files 9 9
Lines 1411 1441 +30
Branches 462 467 +5
==========================================
+ Hits 965 994 +29
- Misses 442 444 +2
+ Partials 4 3 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request is a significant and valuable update. It adds many missing platform-specific fields to the IAP types, improving parity between iOS and Android. The refactoring to remove as any casts and introduce a typed object for oneTimePurchaseOfferDetailsAndroid greatly enhances type safety. The addition of the global DataModalContext in the example apps is a nice touch for improving the developer experience.
My review focuses on a few areas in the example apps:
- A regression in
purchase-flow.tsxwhere filtering for owned products was removed. - An opportunity to refactor
subscription-flow.tsxto reduce prop drilling and improve maintainability. - A couple of minor type safety improvements and inconsistencies in the example app UI.
Overall, the core library changes are excellent. The feedback is mostly centered on making the example app code more robust and consistent.
| const visibleProducts = products; | ||
| const hasHiddenNonConsumables = false; |
There was a problem hiding this comment.
The logic to filter out and hide already-owned non-consumable products from the list of available products has been removed. This appears to be a regression in the example app's functionality, as it will now show products that the user has already purchased. Was this removal intentional? If not, please consider re-introducing this filtering logic.
| setPurchaseDetailsTarget(lastPurchase); | ||
| setPurchaseDetailsVisible(true); | ||
| }} | ||
| onPress={() => {}} |
There was a problem hiding this comment.
The onPress handler for the PurchaseSummaryRow displaying the lastPurchase is currently a no-op (() => {}). For consistency with the changes in available-purchases.tsx, this should use the new DataModalContext to display the purchase details. You would need to get showData from useDataModal in the container and pass it down as a prop.
| setPurchaseDetailsVisible(true); | ||
| }} | ||
| /> | ||
| <PurchaseSummaryRow purchase={lastPurchase} onPress={() => {}} /> |
There was a problem hiding this comment.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting
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)
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (1)
521-529: Bug: remove*Listener clears all listenersBoth removePurchaseUpdatedListener and removePurchaseErrorListener call clear(), removing unrelated listeners. Remove only the provided listener.
Apply this diff:
- override fun removePurchaseUpdatedListener(listener: (purchase: NitroPurchase) -> Unit) { - // Note: Kotlin doesn't have easy closure comparison, so we'll clear all listeners - purchaseUpdatedListeners.clear() - } + override fun removePurchaseUpdatedListener(listener: (purchase: NitroPurchase) -> Unit) { + purchaseUpdatedListeners.remove(listener) + } - override fun removePurchaseErrorListener(listener: (error: NitroPurchaseResult) -> Unit) { - // Note: Kotlin doesn't have easy closure comparison, so we'll clear all listeners - purchaseErrorListeners.clear() - } + override fun removePurchaseErrorListener(listener: (error: NitroPurchaseResult) -> Unit) { + purchaseErrorListeners.remove(listener) + }
🧹 Nitpick comments (6)
example-expo/app/purchase-flow.tsx (1)
309-364: Optional: Consider using the shared DataModal instead of a local ModalTo align examples with the new global DataModalProvider, route Product Details through the shared modal for consistency and reuse.
src/specs/RnIap.nitro.ts (2)
304-313: Fix JSDoc: return type mismatch for requestPurchaseComment says Promise but signature returns Promise. Update docs to prevent confusion for API users.
Apply this diff to the doc:
/** * Request a purchase (unified method for both platforms) * ⚠️ Important: This is an event-based operation, not promise-based. * Listen for events through purchaseUpdatedListener or purchaseErrorListener. * @param request - Platform-specific purchase request parameters - * @returns Promise<void> - Always returns void, listen for events instead + * @returns Promise<RequestPurchaseResult> - Do not rely on this for flow; listen to purchase events */
182-190: Document micros semantics for Android pricing fieldsClarify that priceAmountMicros is a string representing micro-units (1e-6), matching Google Play Billing. Helps downstream consumers parse consistently.
Apply this diff to add inline docs:
export interface NitroOneTimePurchaseOfferDetail { - formattedPrice: string; - priceAmountMicros: string; - priceCurrencyCode: string; + /** Localized formatted price (e.g., "$1.99") */ + formattedPrice: string; + /** Microunits as string (e.g., "1990000" == 1.99) */ + priceAmountMicros: string; + /** ISO 4217 currency code (e.g., "USD") */ + priceCurrencyCode: string; }android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (1)
577-591: Unused helper: createPurchaseErrorResultNot referenced; consider removing to reduce surface and confusion with toErrorResult().
example-expo/app/subscription-flow.tsx (2)
621-623: Nit: avoid parseInt on a number-typed fieldsubscriptionPeriodNumberIOS is numeric; prefer Number() to avoid string parsing semantics.
Apply this diff:
- const periodNum = parseInt(periodNumber, 10); + const periodNum = Number(periodNumber);
486-500: Optional: replace magic number proration mode with a named constantIf available from your types/SDK, use a named enum/constant (e.g., IMMEDIATE_WITHOUT_PRORATION) instead of 5 for readability.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
example/ios/Podfile.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt(5 hunks)example-expo/app/_layout.tsx(1 hunks)example-expo/app/available-purchases.tsx(3 hunks)example-expo/app/purchase-flow.tsx(11 hunks)example-expo/app/subscription-flow.tsx(14 hunks)example-expo/contexts/DataModalContext.tsx(1 hunks)example-expo/scripts/copy-screens.sh(1 hunks)example/App.tsx(1 hunks)example/__tests__/screens/AvailablePurchases.test.tsx(6 hunks)example/screens/AvailablePurchases.tsx(3 hunks)example/src/contexts/DataModalContext.tsx(1 hunks)ios/RnIapHelper.swift(2 hunks)src/specs/RnIap.nitro.ts(6 hunks)src/utils/type-bridge.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use type-only imports when importing types (import type).
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/AvailablePurchases.tsxexample-expo/app/_layout.tsxexample-expo/contexts/DataModalContext.tsxexample-expo/app/available-purchases.tsxsrc/specs/RnIap.nitro.tsexample/App.tsxexample-expo/app/purchase-flow.tsxexample-expo/app/subscription-flow.tsxsrc/utils/type-bridge.ts
{src,example,example-expo}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{src,example,example-expo}/**/*.{ts,tsx}: When declaring API params/results in TS modules, import canonical types from src/types.ts rather than creating custom interfaces.
Handle errors using the shared utilities (parseErrorStringToJsonObj, isUserCancelledError) and standardized ErrorCode enum.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/AvailablePurchases.tsxexample-expo/app/_layout.tsxexample-expo/contexts/DataModalContext.tsxexample-expo/app/available-purchases.tsxsrc/specs/RnIap.nitro.tsexample/App.tsxexample-expo/app/purchase-flow.tsxexample-expo/app/subscription-flow.tsxsrc/utils/type-bridge.ts
{src,example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Platform.OS checks for platform-specific logic in React Native code.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/AvailablePurchases.tsxexample-expo/app/_layout.tsxexample-expo/contexts/DataModalContext.tsxexample-expo/app/available-purchases.tsxsrc/specs/RnIap.nitro.tsexample/App.tsxexample-expo/app/purchase-flow.tsxexample-expo/app/subscription-flow.tsxsrc/utils/type-bridge.ts
{example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/AvailablePurchases.tsxexample-expo/app/_layout.tsxexample-expo/contexts/DataModalContext.tsxexample-expo/app/available-purchases.tsxexample/App.tsxexample-expo/app/purchase-flow.tsxexample-expo/app/subscription-flow.tsx
ios/**/*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Follow the native class function ordering in Swift: properties/init; public cross-platform methods; platform-specific public methods; event listener methods; private helpers.
Files:
ios/RnIapHelper.swift
android/src/main/java/com/margelo/nitro/iap/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
android/src/main/java/com/margelo/nitro/iap/**/*.kt: Follow the native class function ordering in Kotlin: properties/init; public cross-platform methods; platform-specific public methods; event listener methods; private helpers.
On Android, rely on automatic service reconnection; only check that the BillingClient exists, not connection state.
Files:
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
src/specs/RnIap.nitro.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Define the Nitro interface (ALL native method declarations) in src/specs/RnIap.nitro.ts as the contract to native code.
Files:
src/specs/RnIap.nitro.ts
src/specs/**/*.nitro.ts
📄 CodeRabbit inference engine (CLAUDE.md)
After modifying any .nitro.ts interface files, regenerate Nitro bridge files (yarn specs).
Files:
src/specs/RnIap.nitro.ts
🧠 Learnings (3)
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to {example,example-expo}/**/*.{ts,tsx,js,jsx} : In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Applied to files:
example/screens/AvailablePurchases.tsxexample-expo/app/available-purchases.tsxexample-expo/app/purchase-flow.tsxexample-expo/app/subscription-flow.tsx
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to android/src/main/java/com/margelo/nitro/iap/**/*.kt : Follow the native class function ordering in Kotlin: properties/init; public cross-platform methods; platform-specific public methods; event listener methods; private helpers.
Applied to files:
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to src/specs/RnIap.nitro.ts : Define the Nitro interface (ALL native method declarations) in src/specs/RnIap.nitro.ts as the contract to native code.
Applied to files:
src/specs/RnIap.nitro.ts
🧬 Code graph analysis (9)
example/__tests__/screens/AvailablePurchases.test.tsx (1)
example/screens/AvailablePurchases.tsx (1)
AvailablePurchases(21-376)
example/screens/AvailablePurchases.tsx (1)
example/src/contexts/DataModalContext.tsx (1)
useDataModal(103-109)
example-expo/app/_layout.tsx (1)
example-expo/contexts/DataModalContext.tsx (1)
DataModalProvider(23-101)
example-expo/app/available-purchases.tsx (1)
example-expo/contexts/DataModalContext.tsx (1)
useDataModal(103-109)
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (1)
src/specs/RnIap.nitro.ts (1)
NitroOneTimePurchaseOfferDetail(185-189)
example/App.tsx (1)
example/src/contexts/DataModalContext.tsx (1)
DataModalProvider(23-101)
example-expo/app/purchase-flow.tsx (2)
src/hooks/useIAP.ts (1)
useIAP(90-397)src/index.ts (3)
useIAP(104-104)getStorefront(564-595)fetchProducts(315-446)
example-expo/app/subscription-flow.tsx (6)
src/types.ts (4)
ProductSubscription(276-276)Purchase(332-332)ActiveSubscription(6-17)PurchaseError(368-372)android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (3)
getAvailablePurchases(377-414)requestPurchase(240-374)fetchProducts(178-236)src/index.ts (5)
getAvailablePurchases(461-517)requestPurchase(879-1000)useIAP(104-104)getActiveSubscriptions(1470-1543)fetchProducts(315-446)ios/HybridRnIap.swift (3)
getAvailablePurchases(227-248)requestPurchase(143-225)fetchProducts(80-141)src/hooks/useIAP.ts (1)
useIAP(90-397)example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)
src/utils/type-bridge.ts (1)
src/types.ts (1)
Product(214-214)
🪛 GitHub Actions: CI - Example Expo
example-expo/app/subscription-flow.tsx
[warning] 35-35: eslint: @typescript-eslint/array-type - Array type using 'Array' is forbidden. Use 'T[]' instead.
[warning] 41-41: eslint: @typescript-eslint/array-type - Array type using 'Array' is forbidden. Use 'T[]' instead.
[error] 80-80: react/display-name: Component definition is missing display name. (eslint rule react/display-name). Command: expo lint
🪛 GitHub Check: example-expo
example-expo/app/subscription-flow.tsx
[failure] 80-80:
Component definition is missing display name
[warning] 41-41:
Array type using 'Array' is forbidden. Use 'T[]' instead
[warning] 35-35:
Array type using 'Array' is forbidden. Use 'T[]' instead
[warning] 527-527:
React Hook useCallback has missing dependencies: 'cachedAvailablePurchases' and 'setCachedAvailablePurchases'. Either include them or remove the dependency array. If 'setCachedAvailablePurchases' changes too often, find the parent component that defines it and wrap that definition in useCallback
⏰ 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). (2)
- GitHub Check: build-android
- GitHub Check: build-ios
🔇 Additional comments (7)
example-expo/app/purchase-flow.tsx (2)
443-454: LGTM: Robust storefront fetch flow with safe fallbacksGood async handling, disabled button state, and normalization to null on blanks. Logs on failure without breaking UI.
421-437: User‑cancel path handled correctlyExplicitly checks ErrorCode.UserCancelled and short‑circuits UX messaging. Nice.
android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (4)
660-666: LGTM: one‑time offer typed mappingNice conversion to NitroOneTimePurchaseOfferDetail; keeps types aligned with the spec.
705-709: LGTM: nameAndroid propagationCorrectly reads nameAndroid from both ProductAndroid and ProductSubscriptionAndroid.
724-742: LGTM: Extended product payload (JSON offers + one‑time offer details)JSON serialization for subscriptionOfferDetailsAndroid and typed oneTimePurchaseOfferDetailsAndroid look good.
793-794: LGTM: developerPayloadAndroid exposedMapping developerPayloadAndroid completes parity with upstream OpenIAP.
example-expo/app/subscription-flow.tsx (1)
1132-1139: Good: refresh both active subs and purchases after successCaching getAvailablePurchases and refreshing state aligns with the hook’s event flow.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
example/screens/SubscriptionFlow.tsx (1)
61-190: Name the memoized component to satisfy react/display-name
expo lintstill fails because thisReact.memocall receives an anonymous arrow. Please switch to a named function when passing it toReact.memo(you can keep the explicitdisplayName). Example fix:-const PlanChangeControls = React.memo<PlanChangeControlsProps>( - ({ - activeSubscriptions, - handlePlanChange, - isProcessing, - lastPurchasedPlan, - }) => { +const PlanChangeControls = React.memo(function PlanChangeControls({ + activeSubscriptions, + handlePlanChange, + isProcessing, + lastPurchasedPlan, +}: PlanChangeControlsProps) { @@ - }, -); +});
♻️ Duplicate comments (1)
example-expo/app/subscription-flow.tsx (1)
61-194: Regenerate this copy after fixing the PlanChangeControls nameThis generated file still has the anonymous
React.memowrapper, so thereact/display-namelint failure will persist here too. After applying the named-function fix inexample/screens/SubscriptionFlow.tsx, rerun the copy step (e.g.,yarn copy-screens) so this file inherits the change.
🧹 Nitpick comments (1)
example/screens/SubscriptionFlow.tsx (1)
84-87: Drop theas anywhen sorting by transactionDate
ActiveSubscriptionalready exposestransactionDate, so we can compare it directly and keep the function fully typed:- const sortedSubs = [...premiumSubs].sort((a, b) => { - const dateA = (a as any).transactionDate || 0; - const dateB = (b as any).transactionDate || 0; + const sortedSubs = [...premiumSubs].sort((a, b) => { + const dateA = a.transactionDate ?? 0; + const dateB = b.transactionDate ?? 0;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.github/workflows/ci-example-expo.yml(1 hunks)example-expo/app/subscription-flow.tsx(13 hunks)example/screens/SubscriptionFlow.tsx(6 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use type-only imports when importing types (import type).
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{src,example,example-expo}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{src,example,example-expo}/**/*.{ts,tsx}: When declaring API params/results in TS modules, import canonical types from src/types.ts rather than creating custom interfaces.
Handle errors using the shared utilities (parseErrorStringToJsonObj, isUserCancelledError) and standardized ErrorCode enum.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{src,example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Platform.OS checks for platform-specific logic in React Native code.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to {example,example-expo}/**/*.{ts,tsx,js,jsx} : In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Applied to files:
example-expo/app/subscription-flow.tsx
🧬 Code graph analysis (2)
example-expo/app/subscription-flow.tsx (5)
src/types.ts (4)
Purchase(332-332)ActiveSubscription(6-17)ProductSubscriptionAndroid(278-292)PurchaseError(368-372)android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (2)
getAvailablePurchases(377-414)requestPurchase(240-374)src/index.ts (4)
getAvailablePurchases(461-517)requestPurchase(879-1000)useIAP(104-104)getActiveSubscriptions(1470-1543)src/hooks/useIAP.ts (1)
useIAP(90-397)example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)
example/screens/SubscriptionFlow.tsx (1)
src/types.ts (1)
ProductSubscriptionAndroid(278-292)
⏰ 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). (2)
- GitHub Check: build-ios
- GitHub Check: build-android
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
example-expo/app/subscription-flow.tsx (2)
1150-1174: AddgetAvailablePurchasesto this effect’s depsThis effect invokes
getAvailablePurchases()but the dependency array omits it, trippingreact-hooks/exhaustive-deps. Append it so lint passes.- }, [connected, fetchProducts]); + }, [connected, fetchProducts, getAvailablePurchases]);
1175-1210: Also listgetAvailablePurchaseshereSame lint rule applies to
handleRefreshStatus; include the function in the dependency list so ESLint stops flagging it.- }, [connected, getActiveSubscriptions, isCheckingStatus]); + }, [connected, getActiveSubscriptions, getAvailablePurchases, isCheckingStatus]);
♻️ Duplicate comments (3)
example-expo/app/subscription-flow.tsx (3)
945-959: Wire the latest purchase row into the DataModal
PurchaseSummaryRowstill has a no-op handler, so tapping the latest purchase yields nothing. Please reuseuseDataModal().showDatahere (as noted earlier) so the Expo flow matches the rest of the app.
61-191: Name the memoized component so react/display-name stops failingExpo lint still flags this anonymous arrow you pass to
React.memo. Please switch to a named function (per the earlier review) so the rule recognizes the display name.-const PlanChangeControls = React.memo<PlanChangeControlsProps>( - ({ - activeSubscriptions, - handlePlanChange, - isProcessing, - lastPurchasedPlan, - }) => { +const PlanChangeControls = React.memo(function PlanChangeControls({ + activeSubscriptions, + handlePlanChange, + isProcessing, + lastPurchasedPlan, +}: PlanChangeControlsProps) { … - }, -); +});
674-679: Drop the new(a as any)casts
ActiveSubscriptionalready exposestransactionDate, so the fresh casts undo the type-safety work in this PR. Read the field directly.- const sortedPremiumSubs = [...premiumSubs].sort((a, b) => { - const dateA = (a as any).transactionDate || 0; - const dateB = (b as any).transactionDate || 0; + const sortedPremiumSubs = [...premiumSubs].sort((a, b) => { + const dateA = a.transactionDate ?? 0; + const dateB = b.transactionDate ?? 0;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
example-expo/app/subscription-flow.tsx(13 hunks)example-expo/contexts/DataModalContext.tsx(1 hunks)example/__tests__/screens/AvailablePurchases.test.tsx(6 hunks)example/screens/SubscriptionFlow.tsx(7 hunks)example/src/contexts/DataModalContext.tsx(1 hunks)src/utils/type-bridge.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use type-only imports when importing types (import type).
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/SubscriptionFlow.tsxsrc/utils/type-bridge.tsexample-expo/app/subscription-flow.tsxexample-expo/contexts/DataModalContext.tsx
{src,example,example-expo}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{src,example,example-expo}/**/*.{ts,tsx}: When declaring API params/results in TS modules, import canonical types from src/types.ts rather than creating custom interfaces.
Handle errors using the shared utilities (parseErrorStringToJsonObj, isUserCancelledError) and standardized ErrorCode enum.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/SubscriptionFlow.tsxsrc/utils/type-bridge.tsexample-expo/app/subscription-flow.tsxexample-expo/contexts/DataModalContext.tsx
{src,example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Platform.OS checks for platform-specific logic in React Native code.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/SubscriptionFlow.tsxsrc/utils/type-bridge.tsexample-expo/app/subscription-flow.tsxexample-expo/contexts/DataModalContext.tsx
{example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Files:
example/__tests__/screens/AvailablePurchases.test.tsxexample/src/contexts/DataModalContext.tsxexample/screens/SubscriptionFlow.tsxexample-expo/app/subscription-flow.tsxexample-expo/contexts/DataModalContext.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to {example,example-expo}/**/*.{ts,tsx,js,jsx} : In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Applied to files:
example-expo/app/subscription-flow.tsx
🧬 Code graph analysis (4)
example/__tests__/screens/AvailablePurchases.test.tsx (1)
example/src/contexts/DataModalContext.tsx (1)
DataModalProvider(30-122)
example/screens/SubscriptionFlow.tsx (1)
src/types.ts (1)
ProductSubscriptionAndroid(278-292)
src/utils/type-bridge.ts (1)
src/types.ts (1)
Product(214-214)
example-expo/app/subscription-flow.tsx (4)
src/types.ts (3)
Purchase(332-332)ActiveSubscription(6-17)ProductSubscriptionAndroid(278-292)src/index.ts (5)
getAvailablePurchases(461-517)requestPurchase(879-1000)useIAP(104-104)getActiveSubscriptions(1470-1543)fetchProducts(315-446)src/hooks/useIAP.ts (1)
useIAP(90-397)example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)
⏰ 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). (3)
- GitHub Check: build-android
- GitHub Check: build-ios
- GitHub Check: example-expo
🔇 Additional comments (7)
example/screens/SubscriptionFlow.tsx (7)
57-63: LGTM! Explicit generic type improves type safety.The explicit generic signature
React.memo<PlanChangeControlsProps>provides better type checking and IDE support.
81-82: LGTM! Safe property access with nullish coalescing.Using
?? 0provides a safe fallback whentransactionDateisnullorundefined.
189-189: LGTM! DisplayName aids debugging.Setting
displayNameon memoized components improves React DevTools output.
360-360: LGTM! Appropriate cast for Android-specific subscription details.The cast to
ProductSubscriptionAndroidis necessary to accesssubscriptionOfferDetailsAndroid, which is Android-specific per the type definition.
506-513: LGTM! Complete useCallback dependency array.The expanded dependencies correctly include
cachedAvailablePurchasesandsetCachedAvailablePurchases, which are both used within the callback (lines 378-383).
1238-1242: LGTM! Type-safe casts for Android subscription offers.The casts to
ProductSubscriptionAndroidare appropriately guarded by first checking for the existence ofsubscriptionOfferDetailsAndroid(line 1237), ensuring type safety before accessing Android-specific properties.
21-21: Use type-only import for ProductSubscriptionAndroid.Per coding guidelines, import types using type-only syntax.
Apply this diff:
type ActiveSubscription, type ProductSubscription, - type ProductSubscriptionAndroid, + type ProductSubscriptionAndroid, type Purchase,Wait, let me check the actual line again. The import already uses
typekeyword. Let me re-examine.Actually, reviewing line 21 again:
type ProductSubscriptionAndroid,- this already uses thetypekeyword prefix for the individual import. This is correct per the guideline. No issue here.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
example/screens/SubscriptionFlow.tsx (1)
278-521: IncludegetAvailablePurchasesin the dependency array.The
handlePlanChangecallback calls the importedgetAvailablePurchasesfunction at line 388, but it is not listed in the dependency array at lines 509-516. WhilegetAvailablePurchasesis a stable import, including it in the dependency array satisfies thereact-hooks/exhaustive-depsrule and makes the dependencies explicit.Apply this diff to add
getAvailablePurchasesto the dependency array:[ subscriptions, activeSubscriptions, setIsProcessing, setPurchaseResult, cachedAvailablePurchases, setCachedAvailablePurchases, + getAvailablePurchases, ],
🧹 Nitpick comments (1)
example/screens/SubscriptionFlow.tsx (1)
699-699: UseExtendedActiveSubscriptioninstead ofanyfor better type safety.Line 699 uses
sub: anyin the map callback, which defeats the type safety improvements in this PR. SinceExtendedActiveSubscriptionis already defined (lines 45-50), use it here for consistency.Apply this diff:
- return subsToShow.map((sub: any, index: number) => { + return subsToShow.map((sub: ExtendedActiveSubscription, index: number) => {Then, remove the cast at line 719:
- const extendedSub = sub as ExtendedActiveSubscription; console.log( 'Product ID:', sub.productId, 'Is Upgraded?:', - extendedSub.isUpgradedIOS, + sub.isUpgradedIOS, );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
example-expo/app/subscription-flow.tsx(13 hunks)example/__tests__/screens/AvailablePurchases.test.tsx(6 hunks)example/screens/SubscriptionFlow.tsx(12 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- example/tests/screens/AvailablePurchases.test.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use type-only imports when importing types (import type).
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{src,example,example-expo}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{src,example,example-expo}/**/*.{ts,tsx}: When declaring API params/results in TS modules, import canonical types from src/types.ts rather than creating custom interfaces.
Handle errors using the shared utilities (parseErrorStringToJsonObj, isUserCancelledError) and standardized ErrorCode enum.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{src,example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Platform.OS checks for platform-specific logic in React Native code.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
{example,example-expo}/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Files:
example-expo/app/subscription-flow.tsxexample/screens/SubscriptionFlow.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-24T00:58:14.901Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.901Z
Learning: Applies to {example,example-expo}/**/*.{ts,tsx,js,jsx} : In useIAP-based UI code, do not expect return values from methods that update internal state (e.g., fetchProducts, requestPurchase); consume state from the hook. Only the documented exceptions return values.
Applied to files:
example-expo/app/subscription-flow.tsx
🧬 Code graph analysis (2)
example-expo/app/subscription-flow.tsx (4)
src/types.ts (3)
Purchase(332-332)ActiveSubscription(6-17)ProductSubscriptionAndroid(278-292)src/index.ts (4)
getAvailablePurchases(461-517)requestPurchase(879-1000)useIAP(104-104)getActiveSubscriptions(1470-1543)src/hooks/useIAP.ts (1)
useIAP(90-397)example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)
example/screens/SubscriptionFlow.tsx (1)
src/types.ts (2)
ActiveSubscription(6-17)ProductSubscriptionAndroid(278-292)
⏰ 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). (2)
- GitHub Check: build-ios
- GitHub Check: build-android
🔇 Additional comments (10)
example/screens/SubscriptionFlow.tsx (9)
21-21: LGTM: Type-only import used correctly.The import of
ProductSubscriptionAndroidcorrectly uses thetypekeyword for type-only imports.
30-37: LGTM: ExtendedPurchase type definition is appropriate.The type definition appropriately extends
Purchasewith optional Android-specific fields needed for plan-change logic.
39-50: LGTM: ExtendedActiveSubscription type is well-documented.The type definition appropriately extends
ActiveSubscriptionwith optional fields for plan detection, and the inline comments clearly explain the purpose of each field.
64-192: LGTM: PlanChangeControls component correctly addresses past review feedback.The component now uses a named function inside
React.memo, resolving the ESLintreact/display-nameissue. The sorting logic at lines 91-92 correctly uses nullish coalescing (?? 0) instead ofas any, improving type safety as recommended in previous reviews.
214-232: LGTM: SubscriptionFlowProps type accurately reflects new functionality.The expanded props correctly support the new plan-change and caching features with appropriate types.
836-841: LGTM: PlanChangeControls is correctly integrated.The component is properly integrated with all required props.
1036-1079: LGTM: Plan detection logic inonPurchaseSuccessis well-structured.The callback correctly detects the purchased plan for both iOS (via
productId) and Android (viaofferTokenmapping), and the cast toProductSubscriptionAndroidat line 1060 is safe within the Android-specific branch.
1117-1126: LGTM: Refresh logic correctly caches available purchases.The callback appropriately refreshes both active subscriptions and available purchases after a successful purchase, and caches the latter for use in plan-change logic.
1184-1191: LGTM: Refresh status logic correctly updates cache.The refresh status handler appropriately updates the cached available purchases, ensuring the cache stays fresh.
example-expo/app/subscription-flow.tsx (1)
1-4: This file is auto-generated fromexample/screens/SubscriptionFlow.tsx.As noted in the header comment (lines 1-3), this file is automatically copied during postinstall. Any changes should be made to the source file
example/screens/SubscriptionFlow.tsx, not here. Review comments for this file apply to the source file.
Summary
discountsIOSfield to Product types for iOS subscription promotional offersas anytype assertions throughout type-bridge for better type safetyChanges
Core Library
iOS (StoreKit 2)
Android (Play Billing)
Type Safety Improvements
as anytype assertions in type-bridge.tsExample Apps
Example (React Native)
Example-Expo
Test Plan
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests
Chores