Skip to content

feat(enh): only include obfuscatedProfileIdAndroid for new purchase#3039

Merged
hyochan merged 6 commits intomainfrom
feat/and-subs-upgrade-downgrade
Sep 29, 2025
Merged

feat(enh): only include obfuscatedProfileIdAndroid for new purchase#3039
hyochan merged 6 commits intomainfrom
feat/and-subs-upgrade-downgrade

Conversation

@hyochan
Copy link
Owner

@hyochan hyochan commented Sep 29, 2025

Check if the subscription is upgrade and filter obfuscatedProfileIdAndroid field which is done in hyodotdev/openiap-google#13.

val isUpgrade = !androidRequest.purchaseTokenAndroid.isNullOrEmpty()

obfuscatedProfileIdAndroid = if (isUpgrade) null else androidRequest.obfuscatedProfileIdAndroid,

Exact line: https://github.com/hyodotdev/openiap-google/blob/c7af9f04fbdeb42721315185d0e4ed3d8a056cfb/openiap/src/main/java/dev/hyo/openiap/OpenIapModule.kt#L309-L313

Add tests and example codes related to this.

Resolve #3032

Summary by CodeRabbit

  • New Features

    • Plan-change UI with upgrade/downgrade controls, current plan, expiry, auto-renew, and transaction display; added yearly subscription option.
  • Documentation

    • Simplified example links and note blocks for Purchase/Subscription/Available-Purchases/Offer-Code guides.
    • Updated API docs and guides to show useIAP invoked with onPurchaseSuccess/onPurchaseError callbacks (removing currentPurchase/currentPurchaseError examples).
  • Tests

    • Expanded subscription tests covering Android flows, ownership states, retries, connection transitions, modals, and logging.
  • Chores

    • Bumped a dependency patch version.

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Documentation 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

Cohort / File(s) Summary
Docs: example links updated
docs/docs/examples/purchase-flow.md, docs/docs/examples/subscription-flow.md, docs/docs/examples/available-purchases.md, docs/docs/examples/offer-code.md
Replaced prose/GitHub source references with single note blocks pointing from example/app/*.tsxexample/screens/*Flow.tsx; removed redundant “View the full example source” blocks and added disclaimers about verbose/vibe-coded examples.
Docs: useIAP API & purchase flow
docs/docs/api/methods/listeners.md, docs/docs/api/use-iap.md, docs/docs/guides/purchases.md, docs/docs/guides/troubleshooting.md, docs/docs/intro.md
Switched docs from exposing currentPurchase/currentPurchaseError to callback-driven useIAP({ onPurchaseSuccess, onPurchaseError }); updated examples and guidance to handle validation/finish flows inside callbacks and removed effect-based currentPurchase handling.
Example: SubscriptionFlow implementation
example/screens/SubscriptionFlow.tsx
Added Android subscription plan-change flow (offer tokens, replacementModeAndroid, getAvailablePurchases), purchase-detection logic (onPurchaseSuccess → lastPurchasedPlan), new props/state (lastPurchasedPlan, cachedAvailablePurchases, setCachedAvailablePurchases, setIsProcessing, setPurchaseResult, setLastPurchasedPlan), UI annotations for current plan/transactions, and related logging/error handling.
Tests: SubscriptionFlow scenarios & platform cases
example/__tests__/screens/SubscriptionFlow.test.tsx
Extended tests: Android upgrade/downgrade confirm flow, empty-state, ownership label, retry loading, connection state transitions, details modal interactions (copy/console/close), console logging; added Platform resets and getAvailablePurchases mocking.
Constants: add yearly product id
example/src/utils/constants.ts
SUBSCRIPTION_PRODUCT_IDS expanded to include 'dev.hyo.martie.premium_year'; DEFAULT_SUBSCRIPTION_PRODUCT_ID remains first entry.
Version map
openiap-versions.json
Bumped google dependency from 1.2.91.2.10.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🎯 feature, 🛠 bugfix, 📗 example

Poem

I nibble tokens, sniff the plan,
Tap upgrade—alert says “Are you a fan?”
Yearly or monthly, I hop and choose,
Links fixed, tests green, no carrot to lose. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The PR title "feat(enh): only include obfuscatedProfileIdAndroid for new purchase" describes a specific Android subscription upgrade fix involving conditional inclusion of obfuscatedProfileIdAndroid. However, the actual changeset consists primarily of documentation refactoring (removing currentPurchase/currentPurchaseError in favor of callbacks), example screen enhancements with Android plan-change UI, test additions, and a dependency version bump. The core native implementation change described in the PR objectives (OpenIapModule.kt modifications) is not present in this changeset. The title appears to describe a different or incomplete changeset, making it misleading relative to the actual changes being merged. Update the PR title to accurately reflect the actual changes in this PR, such as "feat: enhance subscription flow with Android plan management and callback-based purchase handling" or split this PR to separate the documentation/API refactoring from the native implementation fix. If the OpenIapModule.kt changes are in a separate repository (openiap-google), clarify the cross-repo dependency and ensure this PR's title describes only the changes in this repository.
Out of Scope Changes Check ⚠️ Warning The linked issue #3032 focuses narrowly on fixing the Android subscription upgrade error by omitting obfuscatedProfileIdAndroid conditionally. However, this PR includes substantial out-of-scope changes: a major API refactoring across all documentation removing currentPurchase and currentPurchaseError in favor of callback-based purchase handling (affecting intro.md, use-iap.md, listeners.md, purchases.md, troubleshooting.md), documentation updates for multiple example guides (purchase-flow.md, subscription-flow.md, available-purchases.md, offer-code.md), a complete rewrite of subscription-flow.md content, extensive UI and logic enhancements in SubscriptionFlow.tsx including iOS plan detection and transaction display, new test coverage for SubscriptionFlow, and addition of a second subscription product ID. These changes represent a significant API evolution and documentation overhaul that extends far beyond the scope of the reported bug fix. Split this PR into focused, single-purpose PRs: one for the Android subscription upgrade fix (with the native implementation and minimal supporting changes), one for the API refactoring to callback-based purchase handling with corresponding documentation updates, and one for the subscription flow example enhancements. This will make each change easier to review, test, and track against its specific objective, and will prevent unrelated changes from being merged together if any component needs revision.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues Check ❓ Inconclusive Issue #3032 requests a fix to prevent "Invalid arguments provided to the API" errors when upgrading Android subscriptions by conditionally omitting obfuscatedProfileIdAndroid when purchaseTokenAndroid is present. The PR objectives describe implementing this fix in OpenIapModule.kt (lines 309-313), but this native code change is not present in the changeset. The changes do include enhanced Android subscription handling in the example (SubscriptionFlow.tsx with handlePlanChange, plan-change controls, and upgrade/downgrade logic) and related tests, which support demonstrating the feature but do not implement the core fix. The dependency version bump (google 1.2.9 → 1.2.10) may contain the native fix in the openiap-google package, but without visibility into that package's changes or explicit confirmation, it's unclear if the primary objective is met. Verify that the google package version bump (1.2.9 → 1.2.10 in openiap-versions.json) contains the OpenIapModule.kt fix described in the PR objectives. If the native fix is in a separate repository/package, document this dependency clearly in the PR description and confirm the version includes the required changes. Alternatively, if the native changes are pending or in a different PR, update the linked issue status and clarify the implementation plan.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/and-subs-upgrade-downgrade

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

Tip

🧪 Early access (models): enabled

We 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:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

@hyochan hyochan added 🍗 enhancement New feature or request 🤖 android Related to android labels Sep 29, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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 obfuscatedProfileIdAndroid field for Android purchases, ensuring it's only present for initial purchases and not for subscription modifications. It also introduces a comprehensive example and test suite for demonstrating and validating subscription upgrade and downgrade flows, particularly on Android, addressing issue #3032. This enhances the robustness and clarity of in-app purchase management within the application.

Highlights

  • Android obfuscatedProfileIdAndroid Logic: The obfuscatedProfileIdAndroid field will now only be included for new Android purchases, and explicitly set to null for subscription upgrades or downgrades. This change is reflected in the updated openiap-google library version 1.2.10.
  • Enhanced Subscription Flow Example: The SubscriptionFlow.tsx example has been significantly updated to demonstrate and test Android subscription upgrade/downgrade functionality, including dynamic UI for plan changes (monthly/yearly) and robust error handling. It now uses getAvailablePurchases to correctly identify purchase tokens for Android plan changes.
  • Comprehensive Subscription Flow Tests: Extensive new tests have been added to SubscriptionFlow.test.tsx to cover various scenarios in the subscription flow, such as plan changes, empty states, already subscribed products, connection changes, and modal interactions, ensuring the reliability of the new features.
  • Documentation and Dependency Updates: Documentation links in purchase-flow.md and subscription-flow.md have been corrected to point to the refactored example paths. Additionally, iOS dependencies (NitroIap and COCOAPODS) have been updated in Podfile.lock.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link

codecov bot commented Sep 29, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.39%. Comparing base (0bbf0de) to head (81ee7a4).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           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           
Flag Coverage Δ
library 68.39% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +520 to +537
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)',
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

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.

Comment on lines +864 to +872
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');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

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.

Comment on lines +1000 to +1007
// 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,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using (global as any).__lastPurchasedPlan to store state is unreliable as it's not persistent across app reloads. This can lead to incorrect plan detection. A persistent storage mechanism like AsyncStorage should be used to ensure the state is preserved.

Comment on lines +185 to +186
const androidOffers = (targetSubscription as any)
.subscriptionOfferDetailsAndroid;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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
};

Comment on lines +207 to +210
const token =
currentPurchase?.purchaseToken ||
(currentPurchase as any)?.purchaseTokenAndroid ||
(currentPurchase as any)?.dataAndroid?.purchaseToken;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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.

}

// Store the detected plan in the sub object for use in button section
(sub as any)._detectedBasePlanId = detectedBasePlanId;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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.

Comment on lines +607 to +664
{(() => {
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>
);
})()}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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.

Copy link

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c00b99 and 65d1b2e.

⛔ Files ignored due to path filters (1)
  • example/ios/Podfile.lock is 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.tsx
  • example/__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.tsx
  • example/__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.tsx
  • example/__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.tsx
  • example/__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.md
  • example/screens/SubscriptionFlow.tsx
  • docs/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

Copy link

@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)
example/screens/SubscriptionFlow.tsx (2)

403-415: Select the correct purchase token deterministically

Finding 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, not isActive. 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 purchaseTokenAndroid and omits obfuscatedProfileIdAndroid.

You can reuse the unskipped test below or extend this one similarly.


392-503: Unskip and fix the upgrade omission test; inject getAvailablePurchases override

Enable the test and ensure getAvailablePurchases is 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 inclusion

This 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

📥 Commits

Reviewing files that changed from the base of the PR and between 65d1b2e and c5495a1.

📒 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.ts
  • example/__tests__/screens/SubscriptionFlow.test.tsx
  • 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/src/utils/constants.ts
  • example/__tests__/screens/SubscriptionFlow.test.tsx
  • 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/src/utils/constants.ts
  • example/__tests__/screens/SubscriptionFlow.test.tsx
  • 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/src/utils/constants.ts
  • example/__tests__/screens/SubscriptionFlow.test.tsx
  • example/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.tsx
  • example/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 mapping

The map now only returns { sku, offerToken }, avoiding clobbering plan state during render. This directly addresses prior feedback.


479-495: Upgrade flow omits obfuscatedProfileIdAndroid correctly

The Android upgrade request includes purchaseTokenAndroid and excludes obfuscatedProfileIdAndroid, 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' to SUBSCRIPTION_PRODUCT_IDS to match the main example.

Copy link

@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: 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 obfuscatedProfileIdAndroid when purchaseTokenAndroid is 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 purchaseTokenAndroid usage but doesn't mention that obfuscatedProfileIdAndroid should 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

📥 Commits

Reviewing files that changed from the base of the PR and between c5495a1 and 81ee7a4.

📒 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.md
  • docs/docs/examples/subscription-flow.md
  • docs/docs/guides/troubleshooting.md
  • docs/docs/intro.md
  • docs/docs/api/methods/listeners.md
  • docs/docs/guides/purchases.md
  • example/screens/SubscriptionFlow.tsx
  • docs/docs/api/use-iap.md
  • docs/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.md
  • docs/docs/intro.md
  • docs/docs/api/methods/listeners.md
  • docs/docs/guides/purchases.md
  • docs/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.md
  • example/screens/SubscriptionFlow.tsx
  • docs/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 onPurchaseSuccess and onPurchaseError, replacing the previous currentPurchase state 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 fetchProducts and requestPurchase return Promise<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 currentPurchase state. This aligns with the broader API migration to onPurchaseSuccess/onPurchaseError callbacks.

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 any casts 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.

@hyochan hyochan merged commit e5aacfd into main Sep 29, 2025
8 checks passed
@hyochan hyochan deleted the feat/and-subs-upgrade-downgrade branch September 29, 2025 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Related to android 🍗 enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Getting Invalid arguments provided to the API when upgrading

1 participant