feat(enh): only include obfuscatedProfileIdAndroid for new purchase#3039
feat(enh): only include obfuscatedProfileIdAndroid for new purchase#3039
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. WalkthroughDocumentation links updated; SubscriptionFlow expanded with Android plan-change logic, purchase-detection, extra state/props, and UI changes; tests extended for platform flows, modals, and edge states; subscription product IDs added; google dependency bumped. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as SubscriptionFlow (UI)
participant IAP as react-native-iap
participant Store as Play / App Store
participant State as Local State
rect rgb(245,248,255)
note over UI: User initiates plan change (Android path)
User->>UI: Tap Upgrade/Downgrade
UI->>User: Show confirmation alert
alt Confirmed
UI->>IAP: getAvailablePurchases()
IAP-->>UI: purchases (tokens)
UI->>IAP: requestPurchase({ type:"subs", request:{ android:{ purchaseTokenAndroid, replacementModeAndroid, subscriptionOffers }}})
IAP->>Store: Start purchase flow
Store-->>IAP: Purchase result
IAP-->>UI: onPurchaseSuccess
UI->>State: setLastPurchasedPlan(...)
UI->>User: Update displayed plan/transactions
else Cancelled/Error
UI-->>User: Show cancel/error
end
end
sequenceDiagram
autonumber
actor User
participant UI as SubscriptionFlow
participant IAP as react-native-iap
note over UI: Initial load / refresh
UI->>IAP: fetchProducts({ skus, type:"subs" })
IAP-->>UI: products
UI-->>User: Show products or "Already Subscribed" / empty state
note over UI: Details modal
User->>UI: Open details modal
UI-->>User: Copy / Console / Close interactions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (3 warnings, 1 inconclusive)
✅ Passed checks (1 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. Tip 🧪 Early access (models): enabledWe are currently testing Sonnet 4.5 code review models, which should lead to better review quality. However, this model may result in higher noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience. Note:
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 refines the handling of the 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✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3039 +/- ##
=======================================
Coverage 68.39% 68.39%
=======================================
Files 9 9
Lines 1411 1411
Branches 462 462
=======================================
Hits 965 965
Misses 442 442
Partials 4 4
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 introduces a significant enhancement to the subscription example by adding an upgrade/downgrade flow. The changes are comprehensive, including updates to the example screen, new tests, and documentation. My review focuses on improving the maintainability, readability, and robustness of the new example code in SubscriptionFlow.tsx. Key suggestions include refactoring complex functions, avoiding anti-patterns like state mutation in render and the use of global variables for state, and improving type safety by avoiding as any.
example/screens/SubscriptionFlow.tsx
Outdated
| else { | ||
| // Try to get from AsyncStorage or localStorage | ||
| const storedPlan = (global as any).__lastPurchasedPlan; | ||
|
|
||
| if (storedPlan === 'premium-year') { | ||
| detectedBasePlanId = 'premium-year'; | ||
| activeOfferLabel = '📅 Yearly Plan'; | ||
| } else { | ||
| // Default to monthly | ||
| detectedBasePlanId = 'premium'; | ||
| activeOfferLabel = '📆 Monthly Plan'; | ||
| } | ||
|
|
||
| console.log( | ||
| 'Detected plan from storage:', | ||
| storedPlan || 'none (defaulting to monthly)', | ||
| ); | ||
| } |
There was a problem hiding this comment.
Using (global as any).__lastPurchasedPlan for state management is unreliable in a React Native application. This global variable is not persistent and its state will be lost whenever the JavaScript bundle is reloaded (e.g., during development with Fast Refresh, or if the OS terminates the app). This can lead to incorrect plan detection for the user. For persistent state that survives app restarts, you should use a dedicated storage solution like AsyncStorage.
| if (purchaseData.offerToken) { | ||
| if (purchaseData.offerToken.includes('premium-year')) { | ||
| (global as any).__lastPurchasedPlan = 'premium-year'; | ||
| console.log('Detected yearly plan from purchase'); | ||
| } else { | ||
| (global as any).__lastPurchasedPlan = 'premium'; | ||
| console.log('Detected monthly plan from purchase'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Using (global as any).__lastPurchasedPlan for state management is unreliable. This global variable is not persistent and its state will be lost when the app is reloaded. This can lead to incorrect behavior. For persisting state across app sessions, a dedicated storage solution like AsyncStorage should be used.
example/screens/SubscriptionFlow.tsx
Outdated
| // Store which base plan is being purchased | ||
| if (itemId === 'dev.hyo.martie.premium') { | ||
| (global as any).__lastPurchasedPlan = offer.basePlanId; | ||
| console.log( | ||
| 'Purchasing plan with basePlanId:', | ||
| offer.basePlanId, | ||
| ); | ||
| } |
example/screens/SubscriptionFlow.tsx
Outdated
| const androidOffers = (targetSubscription as any) | ||
| .subscriptionOfferDetailsAndroid; |
There was a problem hiding this comment.
Using as any bypasses TypeScript's type safety, which can hide bugs and make the code harder to refactor. It seems that subscriptionOfferDetailsAndroid is a valid property on Android. To improve type safety, consider defining a local type that extends the ProductSubscription from the library with the expected Android-specific properties.
For example:
type AndroidProductSubscription = ProductSubscription & {
subscriptionOfferDetailsAndroid?: any[]; // Replace 'any' with a more specific type if possible
};
example/screens/SubscriptionFlow.tsx
Outdated
| const token = | ||
| currentPurchase?.purchaseToken || | ||
| (currentPurchase as any)?.purchaseTokenAndroid || | ||
| (currentPurchase as any)?.dataAndroid?.purchaseToken; |
There was a problem hiding this comment.
This logic to retrieve the purchase token chains through multiple possible properties (purchaseToken, purchaseTokenAndroid, dataAndroid.purchaseToken) and uses as any. This suggests that the Purchase type from the library may be incomplete or that the data structure is inconsistent. To improve robustness and type safety, the Purchase type should be augmented to correctly reflect all possible locations for the purchase token. This would eliminate the need for as any and make the code more self-documenting.
example/screens/SubscriptionFlow.tsx
Outdated
| } | ||
|
|
||
| // Store the detected plan in the sub object for use in button section | ||
| (sub as any)._detectedBasePlanId = detectedBasePlanId; |
There was a problem hiding this comment.
Mutating the sub object, which is an item from component state (activeSubscriptions), directly within a render function is an anti-pattern in React. This can lead to unpredictable rendering behavior and bugs that are hard to trace. Instead of mutation, you should compute derived state immutably. A better approach would be to map over activeSubscriptions before the return statement to create a new array of enriched objects that includes the derived detectedBasePlanId, and then use that new array for rendering.
example/screens/SubscriptionFlow.tsx
Outdated
| {(() => { | ||
| const premiumSub = activeSubscriptions.find( | ||
| (sub) => sub.productId === 'dev.hyo.martie.premium', | ||
| ); | ||
| if (!premiumSub) return null; | ||
|
|
||
| // Get the detected base plan (set in the status display section above) | ||
| const currentBasePlan = | ||
| (premiumSub as any)._detectedBasePlanId || 'unknown'; | ||
|
|
||
| console.log('Button section - current base plan:', currentBasePlan); | ||
|
|
||
| return ( | ||
| <View style={styles.planChangeSection}> | ||
| {currentBasePlan === 'premium' && ( | ||
| <TouchableOpacity | ||
| style={[styles.changePlanButton, styles.upgradeButton]} | ||
| onPress={() => | ||
| handlePlanChange( | ||
| 'dev.hyo.martie.premium', | ||
| 'upgrade', | ||
| 'premium', | ||
| ) | ||
| } | ||
| disabled={isProcessing} | ||
| > | ||
| <Text style={styles.changePlanButtonText}> | ||
| ⬆️ Upgrade to Yearly Plan | ||
| </Text> | ||
| <Text style={styles.changePlanButtonSubtext}> | ||
| Save with annual billing | ||
| </Text> | ||
| </TouchableOpacity> | ||
| )} | ||
|
|
||
| {Platform.OS === 'android' && sub.isActive !== undefined && ( | ||
| <View style={styles.statusRow}> | ||
| <Text style={styles.statusLabel}>Auto-Renew:</Text> | ||
| <Text | ||
| style={[ | ||
| styles.statusValue, | ||
| sub.isActive | ||
| ? styles.activeStatus | ||
| : styles.cancelledStatus, | ||
| ]} | ||
| > | ||
| {sub.isActive ? '✅ Enabled' : '⚠️ Cancelled'} | ||
| {currentBasePlan === 'premium-year' && ( | ||
| <TouchableOpacity | ||
| style={[styles.changePlanButton, styles.downgradeButton]} | ||
| onPress={() => | ||
| handlePlanChange( | ||
| 'dev.hyo.martie.premium', | ||
| 'downgrade', | ||
| 'premium-year', | ||
| ) | ||
| } | ||
| disabled={isProcessing} | ||
| > | ||
| <Text style={styles.changePlanButtonText}> | ||
| ⬇️ Downgrade to Monthly Plan | ||
| </Text> | ||
| </View> | ||
| <Text style={styles.changePlanButtonSubtext}> | ||
| More flexibility with monthly billing | ||
| </Text> | ||
| </TouchableOpacity> | ||
| )} | ||
| </View> | ||
| ))} | ||
| </View> | ||
| ); | ||
| })()} |
There was a problem hiding this comment.
This immediately-invoked function expression (IIFE) contains complex conditional rendering logic. While functional, it can make the main component's return statement harder to read. To improve readability and separation of concerns, consider extracting this logic into a new, dedicated React component, for example PlanChangeControls. This new component could be memoized with React.memo for performance.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 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 (5)
docs/docs/examples/purchase-flow.md(1 hunks)docs/docs/examples/subscription-flow.md(1 hunks)example/__tests__/screens/SubscriptionFlow.test.tsx(2 hunks)example/screens/SubscriptionFlow.tsx(9 hunks)openiap-versions.json(1 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/screens/SubscriptionFlow.tsxexample/__tests__/screens/SubscriptionFlow.test.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/screens/SubscriptionFlow.tsxexample/__tests__/screens/SubscriptionFlow.test.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/screens/SubscriptionFlow.tsxexample/__tests__/screens/SubscriptionFlow.test.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/screens/SubscriptionFlow.tsxexample/__tests__/screens/SubscriptionFlow.test.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-24T00:58:14.889Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.889Z
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:
docs/docs/examples/subscription-flow.mdexample/screens/SubscriptionFlow.tsxdocs/docs/examples/purchase-flow.md
📚 Learning: 2025-09-14T00:13:04.055Z
Learnt from: hyochan
PR: hyochan/react-native-iap#3002
File: docs/docs/getting-started/setup-ios.md:55-60
Timestamp: 2025-09-14T00:13:04.055Z
Learning: The validateReceipt function from useIAP hook in react-native-iap expects a transaction identifier as parameter, which is accessed via purchase.id (not purchase.productId). This is confirmed by the maintainer hyochan and aligns with the library's migration from purchase.transactionId to purchase.id.
Applied to files:
docs/docs/examples/purchase-flow.md
🧬 Code graph analysis (2)
example/screens/SubscriptionFlow.tsx (3)
src/index.ts (2)
getAvailablePurchases(461-517)requestPurchase(879-1000)android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (2)
getAvailablePurchases(377-414)requestPurchase(240-374)src/types.ts (1)
PurchaseError(368-372)
example/__tests__/screens/SubscriptionFlow.test.tsx (3)
src/index.ts (1)
fetchProducts(315-446)example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)example/src/utils/constants.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). (1)
- GitHub Check: build-ios
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
example/screens/SubscriptionFlow.tsx (2)
403-415: Select the correct purchase token deterministicallyFinding the first purchase by productId is fragile when multiple purchases exist. Prefer the most recent transaction and then read all known token fields.
- const currentPurchase = availablePurchases.find( - (p: Purchase) => p.productId === currentProductId, - ) as ExtendedPurchase | undefined; - - // Check multiple possible token fields - const extendedPurchase = currentPurchase as - | ExtendedPurchase - | undefined; - const token = - extendedPurchase?.purchaseToken || - extendedPurchase?.purchaseTokenAndroid || - extendedPurchase?.dataAndroid?.purchaseToken; + const candidates = (availablePurchases as ExtendedPurchase[]) + .filter((p) => p.productId === currentProductId) + .sort( + (a, b) => + (b.transactionDate ?? 0) - (a.transactionDate ?? 0), + ); + const currentPurchase = candidates[0]; + const token = + currentPurchase?.purchaseToken ?? + currentPurchase?.purchaseTokenAndroid ?? + currentPurchase?.dataAndroid?.purchaseToken;Also applies to: 436-451
814-829: Show true auto‑renew status on Android (not isActive)“Auto‑Renew” should reflect
autoRenewingAndroid/isAutoRenewing, notisActive. As per coding guidelines.- {Platform.OS === 'android' && - sub.isActive !== undefined && ( + {Platform.OS === 'android' && ( <View style={styles.statusRow}> <Text style={styles.statusLabel}>Auto-Renew:</Text> <Text style={[ styles.statusValue, - sub.isActive + ((sub as any).autoRenewingAndroid ?? + (sub as any).isAutoRenewing) ? styles.activeStatus : styles.cancelledStatus, ]} > - {sub.isActive ? '✅ Enabled' : '⚠️ Cancelled'} + {((sub as any).autoRenewingAndroid ?? + (sub as any).isAutoRenewing) + ? '✅ Enabled' + : '⚠️ Cancelled'} </Text> </View> - )} + )}example/__tests__/screens/SubscriptionFlow.test.tsx (3)
193-267: Strengthen Android plan‑change test to assert payload (simulate Confirm)Currently this test only checks that the confirmation alert appears. Simulate user confirmation and assert the Android payload carries
purchaseTokenAndroidand omitsobfuscatedProfileIdAndroid.You can reuse the unskipped test below or extend this one similarly.
392-503: Unskip and fix the upgrade omission test; inject getAvailablePurchases overrideEnable the test and ensure
getAvailablePurchasesis actually used by the hook mock so the flow can resolve a token and call requestPurchase with the expected fields. Based on learnings.- it.skip('excludes obfuscatedProfileIdAndroid for subscription upgrades/downgrades (Android)', async () => { + it('excludes obfuscatedProfileIdAndroid for subscription upgrades/downgrades (Android)', async () => { // Mock Platform to be Android Platform.OS = 'android'; - // Mock getAvailablePurchases to return purchase with token - jest.fn(() => { - console.log('Test: getAvailablePurchases called'); - return Promise.resolve([ - { - productId: 'dev.hyo.martie.premium', - purchaseToken: 'mock-purchase-token-123', - purchaseTokenAndroid: 'mock-purchase-token-123', - purchaseState: 1, - }, - ]); - }); + // Mock getAvailablePurchases to return purchase with token + const getAvailablePurchases = jest.fn().mockResolvedValue([ + { + productId: 'dev.hyo.martie.premium', + purchaseToken: 'mock-purchase-token-123', + purchaseTokenAndroid: 'mock-purchase-token-123', + transactionDate: Date.now(), + purchaseState: 1, + }, + ]); // For upgrade/downgrade, purchaseTokenAndroid should be included but obfuscatedProfileIdAndroid should not - mockIapState({ + mockIapState({ + getAvailablePurchases, activeSubscriptions: [ { productId: 'dev.hyo.martie.premium', transactionId: 'trans-1', transactionDate: Date.now(), isActive: true, } as any, ], subscriptions: [ { ...sampleSubscription, subscriptionOfferDetailsAndroid: [ { basePlanId: 'premium', offerToken: 'offer-token-monthly', offerTags: [], pricingPhases: { pricingPhaseList: [ { formattedPrice: '$9.99', priceAmountMicros: '9990000', priceCurrencyCode: 'USD', billingPeriod: 'P1M', billingCycleCount: 0, recurrenceMode: 1, }, ], }, }, { basePlanId: 'premium-year', offerToken: 'offer-token-yearly', offerTags: [], pricingPhases: { pricingPhaseList: [ { formattedPrice: '$99.99', priceAmountMicros: '99990000', priceCurrencyCode: 'USD', billingPeriod: 'P1Y', billingCycleCount: 0, recurrenceMode: 1, }, ], }, }, ], }, ], }); const alertSpy = jest.spyOn(Alert, 'alert'); const {getByText} = render(<SubscriptionFlow />); // Wait for upgrade button to appear await waitFor(() => { expect(getByText('⬆️ Upgrade to Yearly Plan')).toBeTruthy(); }); // Mock alert to immediately simulate user confirmation alertSpy.mockImplementation((_title, _message, buttons) => { // Simulate user clicking "Confirm" button (second button) if (buttons && buttons[1] && buttons[1].onPress) { const onPress = buttons[1].onPress; // Execute the onPress callback asynchronously to simulate real behavior setImmediate(() => onPress()); } }); // Press upgrade button fireEvent.press(getByText('⬆️ Upgrade to Yearly Plan')); // Wait for requestPurchase to be called with proper parameters await waitFor( () => { expect(requestPurchaseMock).toHaveBeenCalled(); const lastCall = requestPurchaseMock.mock.calls[ requestPurchaseMock.mock.calls.length - 1 ]; expect(lastCall).toBeDefined(); const androidRequest = lastCall[0].request?.android; // Should have purchaseTokenAndroid for upgrade expect(androidRequest?.purchaseTokenAndroid).toBe( 'mock-purchase-token-123', ); // Should NOT have obfuscatedProfileIdAndroid for upgrade expect(androidRequest?.obfuscatedProfileIdAndroid).toBeUndefined(); }, {timeout: 3000}, ); });
505-527: Rename test to match actual assertion and avoid implying inclusionThis test asserts that no purchaseTokenAndroid is sent for new purchases; rename to reflect that, since example code doesn’t send obfuscatedProfileIdAndroid either.
- it('includes obfuscatedProfileIdAndroid for new subscriptions', () => { + it('does not include purchaseTokenAndroid for new subscriptions (no upgrade)', () => {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
example/__tests__/screens/SubscriptionFlow.test.tsx(6 hunks)example/screens/SubscriptionFlow.tsx(15 hunks)example/src/utils/constants.ts(1 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/src/utils/constants.tsexample/__tests__/screens/SubscriptionFlow.test.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/src/utils/constants.tsexample/__tests__/screens/SubscriptionFlow.test.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/src/utils/constants.tsexample/__tests__/screens/SubscriptionFlow.test.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/src/utils/constants.tsexample/__tests__/screens/SubscriptionFlow.test.tsxexample/screens/SubscriptionFlow.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-24T00:58:14.889Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.889Z
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/__tests__/screens/SubscriptionFlow.test.tsxexample/screens/SubscriptionFlow.tsx
📚 Learning: 2025-09-13T01:07:18.841Z
Learnt from: hyochan
PR: hyochan/react-native-iap#2999
File: android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt:644-660
Timestamp: 2025-09-13T01:07:18.841Z
Learning: In Android IAP error handling: purchaseToken and productId are distinct properties - purchaseToken identifies a completed purchase transaction (should be null in error cases), while productId is the product SKU for context
Applied to files:
example/screens/SubscriptionFlow.tsx
🧬 Code graph analysis (3)
example/src/utils/constants.ts (1)
example-expo/constants/products.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-17)
example/__tests__/screens/SubscriptionFlow.test.tsx (2)
src/index.ts (3)
getActiveSubscriptions(1470-1543)getAvailablePurchases(461-517)fetchProducts(315-446)example/src/utils/constants.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-20)
example/screens/SubscriptionFlow.tsx (4)
src/types.ts (3)
ProductSubscription(276-276)Purchase(332-332)ActiveSubscription(6-17)src/index.ts (3)
getAvailablePurchases(461-517)requestPurchase(879-1000)getActiveSubscriptions(1470-1543)android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (2)
getAvailablePurchases(377-414)requestPurchase(240-374)example/src/utils/constants.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-20)
⏰ 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: build-ios
🔇 Additional comments (3)
example/screens/SubscriptionFlow.tsx (2)
1245-1253: Nice: removed global plan mutation in offer mappingThe map now only returns
{ sku, offerToken }, avoiding clobbering plan state during render. This directly addresses prior feedback.
479-495: Upgrade flow omits obfuscatedProfileIdAndroid correctlyThe Android upgrade request includes
purchaseTokenAndroidand excludesobfuscatedProfileIdAndroid, matching the PR objective.If you’d like, enable the skipped test that asserts this payload (see suggested test diff in the test file comment).
example/src/utils/constants.ts (1)
17-20: Align example-expo subscription SKUs
example-expo/constants/products.ts (lines 17–18) still exports only'dev.hyo.martie.premium'. Confirm if omitting the yearly SKU is intentional; otherwise, add'dev.hyo.martie.premium_year'toSUBSCRIPTION_PRODUCT_IDSto match the main example.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
docs/docs/examples/available-purchases.md (1)
15-17: Consider more professional disclaimer language.The phrase "heavily vibe-coded with Claude and is quite verbose/messy" is informal and may undermine user confidence in the example code. Consider rephrasing to maintain professionalism while setting appropriate expectations.
-:::note -The complete working example can be found at [example/screens/AvailablePurchases.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/AvailablePurchases.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. -::: +:::note +The complete working example can be found at [example/screens/AvailablePurchases.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/AvailablePurchases.tsx). Note that the example code is intentionally verbose for demonstration purposes and should be adapted to your specific use case. +:::docs/docs/examples/offer-code.md (1)
15-17: Consider more professional disclaimer language.Same concern as in available-purchases.md: "heavily vibe-coded with Claude and is quite verbose/messy" is informal and may reduce confidence. Consider rephrasing to maintain professionalism.
-:::note -The complete working example can be found at [example/screens/OfferCode.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/OfferCode.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. -::: +:::note +The complete working example can be found at [example/screens/OfferCode.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/OfferCode.tsx). Note that the example code is intentionally verbose for demonstration purposes and should be adapted to your specific use case. +:::docs/docs/examples/subscription-flow.md (2)
15-19: Consider more professional disclaimer language.Same informal language pattern: "heavily vibe-coded with Claude and is quite verbose/messy". Consider rephrasing for consistency with professional documentation standards.
-:::note -The complete working example can be found at [example/screens/SubscriptionFlow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/SubscriptionFlow.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. -::: +:::note +The complete working example can be found at [example/screens/SubscriptionFlow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/SubscriptionFlow.tsx). Note that the example code is intentionally verbose for demonstration purposes and should be adapted to your specific use case. +:::
339-406: Consider documenting the obfuscatedProfileIdAndroid omission for upgrades.According to the PR objectives, this PR implements logic to omit
obfuscatedProfileIdAndroidwhenpurchaseTokenAndroidis present (i.e., for upgrades). This is the fix for issue #3032 where including both parameters caused "Invalid arguments provided to the API" errors.The Android upgrade/downgrade section documents
purchaseTokenAndroidusage but doesn't mention thatobfuscatedProfileIdAndroidshould be omitted during upgrades. Consider adding a note to help developers avoid this common pitfall.Add a note in this section or in the code example comments:
// Step 4: Request purchase with the old purchase token for replacement await requestPurchase({ request: { ios: { sku: newProductId, }, android: { skus: [newProductId], subscriptionOffers, // IMPORTANT: Include purchase token for subscription replacement purchaseTokenAndroid: currentPurchase.purchaseToken, // Note: obfuscatedProfileIdAndroid should be omitted when upgrading // (i.e., when purchaseTokenAndroid is present) to avoid API errors // Optional: Specify proration mode replacementModeAndroid: changeType === 'upgrade' ? 'IMMEDIATE_WITH_TIME_PRORATION' : 'DEFERRED', // Downgrade happens at next renewal }, }, type: 'subs', });Or add a callout note after the code block explaining this behavior. This would help prevent the exact issue that PR #3032 addressed.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
docs/docs/api/methods/listeners.md(1 hunks)docs/docs/api/use-iap.md(2 hunks)docs/docs/examples/available-purchases.md(1 hunks)docs/docs/examples/offer-code.md(1 hunks)docs/docs/examples/purchase-flow.md(1 hunks)docs/docs/examples/subscription-flow.md(1 hunks)docs/docs/guides/purchases.md(3 hunks)docs/docs/guides/troubleshooting.md(2 hunks)docs/docs/intro.md(2 hunks)example/screens/SubscriptionFlow.tsx(15 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/docs/examples/purchase-flow.md
🧰 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/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/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/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/screens/SubscriptionFlow.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-24T00:58:14.889Z
Learnt from: CR
PR: hyochan/react-native-iap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-24T00:58:14.889Z
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:
docs/docs/examples/available-purchases.mddocs/docs/examples/subscription-flow.mddocs/docs/guides/troubleshooting.mddocs/docs/intro.mddocs/docs/api/methods/listeners.mddocs/docs/guides/purchases.mdexample/screens/SubscriptionFlow.tsxdocs/docs/api/use-iap.mddocs/docs/examples/offer-code.md
📚 Learning: 2025-09-14T00:13:04.055Z
Learnt from: hyochan
PR: hyochan/react-native-iap#3002
File: docs/docs/getting-started/setup-ios.md:55-60
Timestamp: 2025-09-14T00:13:04.055Z
Learning: The validateReceipt function from useIAP hook in react-native-iap expects a transaction identifier as parameter, which is accessed via purchase.id (not purchase.productId). This is confirmed by the maintainer hyochan and aligns with the library's migration from purchase.transactionId to purchase.id.
Applied to files:
docs/docs/guides/troubleshooting.mddocs/docs/intro.mddocs/docs/api/methods/listeners.mddocs/docs/guides/purchases.mddocs/docs/api/use-iap.md
📚 Learning: 2025-09-13T01:07:18.841Z
Learnt from: hyochan
PR: hyochan/react-native-iap#2999
File: android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt:644-660
Timestamp: 2025-09-13T01:07:18.841Z
Learning: In Android IAP error handling: purchaseToken and productId are distinct properties - purchaseToken identifies a completed purchase transaction (should be null in error cases), while productId is the product SKU for context
Applied to files:
docs/docs/intro.mdexample/screens/SubscriptionFlow.tsxdocs/docs/api/use-iap.md
🧬 Code graph analysis (1)
example/screens/SubscriptionFlow.tsx (3)
src/types.ts (4)
ProductSubscription(276-276)Purchase(332-332)ActiveSubscription(6-17)PurchaseError(368-372)src/index.ts (3)
getAvailablePurchases(461-517)requestPurchase(879-1000)getActiveSubscriptions(1470-1543)example/src/utils/constants.ts (1)
SUBSCRIPTION_PRODUCT_IDS(17-20)
🪛 markdownlint-cli2 (0.18.1)
docs/docs/examples/subscription-flow.md
601-601: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
629-629: Files should end with a single newline character
(MD047, single-trailing-newline)
⏰ 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: build-ios
🔇 Additional comments (19)
docs/docs/api/methods/listeners.md (1)
382-389: LGTM!The updated example correctly demonstrates the new callback-driven pattern with
onPurchaseSuccessandonPurchaseError, replacing the previouscurrentPurchasestate polling approach. This aligns well with the broader API migration documented across the PR.docs/docs/examples/subscription-flow.md (1)
21-624: Excellent comprehensive subscription flow documentation.This is a thorough rewrite that significantly improves the subscription flow documentation. The platform-specific sections clearly explain the differences between iOS automatic subscription group handling and Android's explicit upgrade/downgrade mechanism with purchase tokens. The unified handler example at the end brings it all together nicely.
docs/docs/api/use-iap.md (2)
26-27: LGTM!The updated hook behavior documentation clearly explains that methods like
fetchProductsandrequestPurchasereturnPromise<void>and update internal state, rather than resolving to data. This is an important clarification for developers migrating from direct function calls to the hook pattern.
222-222: LGTM!The comment update correctly reflects the new callback-driven pattern, removing references to listening via
currentPurchasestate. This aligns with the broader API migration toonPurchaseSuccess/onPurchaseErrorcallbacks.docs/docs/intro.md (2)
119-156: LGTM! Callback-driven purchase flow documented correctly.The new onPurchaseSuccess and onPurchaseError callbacks are properly documented with correct error handling patterns and receipt validation guidance.
163-254: LGTM! Complete example aligns with new API.The full example correctly demonstrates the callback-driven purchase flow with proper error handling and transaction completion.
docs/docs/guides/purchases.md (3)
160-224: LGTM! Comprehensive callback-driven purchase implementation.The hook-based implementation correctly demonstrates state management, receipt validation, and error handling with the new onPurchaseSuccess/onPurchaseError callbacks.
274-343: LGTM! Purchase request handlers follow best practices.The handlers correctly set loading state before requestPurchase and rely on hook callbacks for state updates, consistent with the useIAP pattern. Based on learnings.
877-890: LGTM! Pending purchase handling is correct.The callback-based pending purchase detection properly returns early to avoid finishing incomplete transactions and provides user feedback.
docs/docs/guides/troubleshooting.md (2)
136-160: LGTM! Troubleshooting example demonstrates correct callback usage.The purchase completion flow properly shows validation and finishing within the onPurchaseSuccess callback with appropriate error handling.
394-406: LGTM! Debug logging pattern is helpful.The callback-based logging examples provide clear guidance for troubleshooting purchase flows.
example/screens/SubscriptionFlow.tsx (8)
29-63: Good type safety improvement for Android-specific properties.The explicit type definitions for Android-specific subscription and purchase properties improve code clarity and reduce reliance on
as anycasts throughout the component.
64-206: Good refactor: plan change controls extracted to dedicated component.The extraction of plan change UI into PlanChangeControls improves readability and addresses previous review feedback. The component correctly handles platform differences and uses React.memo for performance.
232-237: Excellent improvement: proper state management via props.Replacing global state with component props (lastPurchasedPlan, cachedAvailablePurchases, etc.) addresses previous concerns about unreliable state management and follows React best practices.
292-524: LGTM! Android plan change correctly omits obfuscatedProfileIdAndroid for upgrades.The handlePlanChange implementation correctly uses purchaseTokenAndroid for subscription upgrades without including obfuscatedProfileIdAndroid, which aligns with the PR objective to fix the "Invalid arguments provided to the API" error. The cached purchase token retrieval improves performance, and error handling is comprehensive.
1038-1081: Excellent fix: plan detection uses offerToken mapping instead of heuristics.The plan detection logic now correctly maps Android offerToken to basePlanId using subscription offer details, addressing previous review concerns about unreliable substring matching. iOS detection via productId is straightforward and correct.
1120-1128: LGTM! Concurrent refresh improves performance.Using Promise.all to refresh both active subscriptions and available purchases concurrently is efficient and ensures the cache is up-to-date for subsequent plan changes.
1166-1177: LGTM! Initial purchase cache population is efficient.Fetching and caching available purchases once on connection improves performance for subsequent plan change operations.
1248-1256: Good fix: removed premature plan tracking from offer mapper.The subscription offer mapping now correctly returns only {sku, offerToken} without setting plan state, addressing previous review feedback. Plan tracking is properly handled in onPurchaseSuccess after purchase completion.
Check if the subscription is
upgradeand filterobfuscatedProfileIdAndroidfield which is done in hyodotdev/openiap-google#13.Add tests and example codes related to this.
Resolve #3032
Summary by CodeRabbit
New Features
Documentation
Tests
Chores