fix(react-native-iap): parse Nitro iOS cancellation errors - #203
Conversation
|
Warning Review limit reached
Next review available in: 26 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds structured parsing for Nitro NSError payloads, suppresses user-cancellation logging in JS wrappers, normalizes iOS NSError failures into typed purchase exceptions, and updates docs for the canonical cancellation code. ChangesError parsing, cancellation handling, and iOS bridge normalization
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant index_ts as index.ts catch block
participant helper as parseErrorAndLogIfNeeded
participant parser as parseErrorStringToJsonObj
participant console as RnIapConsole
App->>index_ts: native call fails
index_ts->>helper: parseErrorAndLogIfNeeded(prefix, error)
helper->>parser: parseErrorStringToJsonObj(error)
parser-->>helper: parsed IapError
helper-->>helper: isUserCancelledError?
alt not user-cancelled
helper->>console: error(prefix, error)
end
helper-->>index_ts: return parsed error
index_ts-->>App: reject with PurchaseError
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces robust error parsing for Nitro NSError strings with embedded JSON payloads on iOS, ensuring that user-cancelled errors are not logged as actual errors. It refactors multiple API endpoints to use the new parseErrorAndLogIfNeeded helper and adds comprehensive test coverage. The review feedback correctly identifies two critical issues in the new parsing logic: a bug in extractBalancedJsonObject where escaped quotes are ignored when isInsideString is false (potentially causing premature parsing termination), and a failure in parseNSErrorJsonPayload to extract the quoted JSON payload if UserInfo= is absent from the error string.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
libraries/react-native-iap/src/utils/error.ts (2)
185-204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication between inline JSON parsing and
parseJsonPayload.The top-level JSON.parse block (lines 186-198) reimplements the same "parse, default code/message, spread rest" pattern already encapsulated in
parseJsonPayload. Reusing the helper here would reduce duplication.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libraries/react-native-iap/src/utils/error.ts` around lines 185 - 204, The top-level JSON parsing in error.ts duplicates the same parse/default/spread logic already handled by parseJsonPayload. Update the errorString handling path to reuse parseJsonPayload instead of calling JSON.parse inline, and keep the fallback flow to parseNSErrorJsonPayload unchanged. Use parseJsonPayload and parseNSErrorJsonPayload as the main reference points when refactoring this branch.
137-151: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
parseStructuredErrordrops extra fields carried on the Error object.
parseJsonPayloadpreserves all parsed properties via...parsedError, butparseStructuredErroronly returns{code, message}, discarding any other own properties (e.g.productId,responseCode) that might be attached directly to theErrorinstance alongsidecode. If a native error ever carries bothcodeand other structured fields this way, they will silently be lost once this path takes priority.♻️ Preserve extra own properties
const parseStructuredError = (error: Error): IapError | null => { const errorWithCode = error as Error & {code?: unknown}; if ( typeof errorWithCode.code === 'string' && errorWithCode.code.length > 0 ) { + const {code, message, ...rest} = errorWithCode as unknown as Record<string, unknown>; return { - code: errorWithCode.code, - message: error.message, + code: errorWithCode.code, + message: error.message, + ...rest, }; } return null; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libraries/react-native-iap/src/utils/error.ts` around lines 137 - 151, parseStructuredError is dropping own properties on Error objects by returning only code and message. Update parseStructuredError in error.ts to mirror parseJsonPayload by preserving any additional enumerable fields from the Error instance (for example productId or responseCode) while still extracting code and message. Keep the code guard in place, but build the returned IapError from the original error object so structured native error data is not lost.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@libraries/react-native-iap/src/utils/error.ts`:
- Around line 185-204: The top-level JSON parsing in error.ts duplicates the
same parse/default/spread logic already handled by parseJsonPayload. Update the
errorString handling path to reuse parseJsonPayload instead of calling
JSON.parse inline, and keep the fallback flow to parseNSErrorJsonPayload
unchanged. Use parseJsonPayload and parseNSErrorJsonPayload as the main
reference points when refactoring this branch.
- Around line 137-151: parseStructuredError is dropping own properties on Error
objects by returning only code and message. Update parseStructuredError in
error.ts to mirror parseJsonPayload by preserving any additional enumerable
fields from the Error instance (for example productId or responseCode) while
still extracting code and message. Keep the code guard in place, but build the
returned IapError from the original error object so structured native error data
is not lost.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8634cc60-9d50-465f-afdb-7fd19607a4d1
📒 Files selected for processing (4)
libraries/react-native-iap/src/__tests__/index.test.tslibraries/react-native-iap/src/__tests__/utils/error.test.tslibraries/react-native-iap/src/index.tslibraries/react-native-iap/src/utils/error.ts
Extends #203 coverage for iOS NSError completions.
Document the assumed React Native 15.3.6 and KMP 2.3.6 patch releases for issue #202.\n\nClarify UserCancelled handling in the error docs and replace the stale TypeScript E_* example values with OpenIAP kebab-case error codes.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt (2)
84-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsolidate the two divergent "string → ErrorCode" resolution strategies.
toPurchaseException()resolves the raw code viarunCatching { ErrorCode.fromJson(raw) }.getOrNull() ?: fallbackCode, while the pre-existingerrorSubscriptionlistener (Line 131-132) resolves it viaErrorCode.entries.find { it.rawValue == codeString } ?: ErrorCode.Unknown. Both exist to do the same job but could diverge in edge cases (e.g. iffromJsonandentries.finddisagree on case sensitivity or unknown-value handling), which is risky specifically for theuser-cancelledcode this PR is fixing. Also, this new complex platform-specific parsing helper has no comments.♻️ Proposed consolidation
+ private fun errorCodeFromRawValue(raw: String?, fallback: ErrorCode = ErrorCode.Unknown): ErrorCode = + raw?.let { runCatching { ErrorCode.fromJson(it) }.getOrNull() } ?: fallback + private fun NSError.toPurchaseException( fallbackCode: ErrorCode = ErrorCode.Unknown ): PurchaseException { - val code = stringFromUserInfo("code") - ?.let { raw -> runCatching { ErrorCode.fromJson(raw) }.getOrNull() } - ?: fallbackCode + val code = errorCodeFromRawValue(stringFromUserInfo("code"), fallbackCode)- val codeString = map["code"] as? String ?: "unknown" - val errorCode = ErrorCode.entries.find { it.rawValue == codeString } ?: ErrorCode.Unknown + val errorCode = errorCodeFromRawValue(map["code"] as? String)As per coding guidelines, "Comment complex logic, especially platform-specific code."
Also applies to: 129-137
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt` around lines 84 - 101, Unify the two `String -> ErrorCode` mappings so `toPurchaseException()` and the `errorSubscription` listener use the same resolution path, avoiding divergent handling for `user-cancelled` and other unknown values. Update `NSError.toPurchaseException` to share the same lookup logic as the listener (or extract a single helper used by both), then keep `fallbackCode` only as the final default. Add a brief comment near `toPurchaseException` explaining the platform-specific parsing and why the shared resolver is used.Source: Coding guidelines
84-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd unit test coverage for
toPurchaseException().This is the core logic fixing issue
#202for the KMP/iOS layer, but no test exercisesNSError.toPurchaseException()/stringFromUserInfo()directly (e.g. constructing anNSErrorwithuserInfo = mapOf("code" to "user-cancelled")and asserting the resultingPurchaseError.code == ErrorCode.UserCancelled). Given this is a regression-prone parsing path, direct coverage would guard against future refactors breaking cancellation detection.Want me to draft an iosTest for this?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt` around lines 84 - 107, Add direct iosTest coverage for NSError.toPurchaseException() and its helper stringFromUserInfo() in InAppPurchaseIOS; the current parsing path is untested. Create an NSError with userInfo values like code, message, productId, and debugMessage, then assert the resulting PurchaseException/PurchaseError fields are mapped correctly, especially ErrorCode.fromJson handling for user-cancelled -> ErrorCode.UserCancelled. Keep the test focused on the named symbols to protect this regression-prone cancellation parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt`:
- Around line 84-101: Unify the two `String -> ErrorCode` mappings so
`toPurchaseException()` and the `errorSubscription` listener use the same
resolution path, avoiding divergent handling for `user-cancelled` and other
unknown values. Update `NSError.toPurchaseException` to share the same lookup
logic as the listener (or extract a single helper used by both), then keep
`fallbackCode` only as the final default. Add a brief comment near
`toPurchaseException` explaining the platform-specific parsing and why the
shared resolver is used.
- Around line 84-107: Add direct iosTest coverage for
NSError.toPurchaseException() and its helper stringFromUserInfo() in
InAppPurchaseIOS; the current parsing path is untested. Create an NSError with
userInfo values like code, message, productId, and debugMessage, then assert the
resulting PurchaseException/PurchaseError fields are mapped correctly,
especially ErrorCode.fromJson handling for user-cancelled ->
ErrorCode.UserCancelled. Keep the test focused on the named symbols to protect
this regression-prone cancellation parsing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a0d2737c-4c55-413a-96b3-288c6fafee8c
📒 Files selected for processing (1)
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
Address review feedback by preserving structured Error fields, reusing the shared JSON payload parser, parsing quoted NSError payloads without UserInfo, and covering escaped UserInfo payloads that contain braces.
There was a problem hiding this comment.
Code Review
This pull request improves iOS error handling and reporting across the React Native and Kotlin Multiplatform (KMP) bridges, specifically ensuring that user cancellation errors are preserved and not logged as service failures. It introduces robust parsing of Nitro NSError strings with embedded JSON payloads, maps native iOS errors to structured exceptions, and updates documentation and tests accordingly. The code review feedback suggests two improvements: making the NSError JSON parser in React Native more robust against quoted strings when extracting the UserInfo payload, and simplifying the KMP stringFromUserInfo helper by removing redundant NSString key lookups since Kotlin/Native automatically bridges these keys to Kotlin strings.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Use one ErrorCode resolver for iOS completion errors and listener dictionaries.\n\nAdd iosSimulatorArm64 coverage for NSError.userInfo mapping into PurchaseException so user-cancelled and structured fields remain preserved.
Handle quoted NSLocalizedDescription JSON payloads in React Native NSError parsing.\n\nSimplify the KMP NSError userInfo lookup per review feedback while keeping the shared ErrorCode resolver and iOS test coverage intact.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/docs/src/pages/docs/errors.tsx (1)
461-494: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a Swift example.
This new section only covers
typescript,kmp, anddart, unlike otherLanguageTabsblocks in this file that includeswift,kotlin,gdscript, andcsharp. Since the fix this section documents is specifically about iOS cancellation handling, a Swift snippet would round out platform coverage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/docs/src/pages/docs/errors.tsx` around lines 461 - 494, The User Cancellation section in the docs page is missing Swift coverage even though it documents iOS cancellation handling. Update the LanguageTabs block in this section to include a Swift snippet alongside the existing typescript, kmp, and dart examples, matching the style used by other LanguageTabs blocks in errors.tsx and using the same cancellation handling concept with ErrorCode.UserCancelled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/docs/src/pages/docs/errors.tsx`:
- Around line 461-494: The User Cancellation section in the docs page is missing
Swift coverage even though it documents iOS cancellation handling. Update the
LanguageTabs block in this section to include a Swift snippet alongside the
existing typescript, kmp, and dart examples, matching the style used by other
LanguageTabs blocks in errors.tsx and using the same cancellation handling
concept with ErrorCode.UserCancelled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 89c3d660-c808-444b-a470-508623b68cb6
📒 Files selected for processing (4)
libraries/react-native-iap/src/__tests__/utils/error.test.tslibraries/react-native-iap/src/utils/error.tspackages/docs/src/pages/docs/errors.tsxpackages/docs/src/pages/docs/updates/releases.tsx
There was a problem hiding this comment.
Code Review
This pull request improves iOS error handling across the React Native and Kotlin Multiplatform (KMP) bridges by ensuring that native NSError objects and Nitro error strings are correctly parsed to preserve structured error codes, particularly for user cancellations (user-cancelled). This prevents expected user cancellations from being logged as service failures or generic errors. Additionally, unit tests and documentation have been updated to reflect these changes. A review comment suggests explicitly checking for NSNull when extracting values from NSError.userInfo in the KMP iOS implementation to avoid serialization issues.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Treat NSNull values in NSError.userInfo as absent fields when building PurchaseException.\n\nExtend iOS error mapping tests to cover null debug metadata.
There was a problem hiding this comment.
Code Review
This pull request improves iOS error mapping and parsing across the React Native and Kotlin Multiplatform (KMP) bridges to ensure native iOS cancellation errors (user-cancelled) are correctly preserved and not logged as failures. In KMP, native NSError values are now mapped to structured PurchaseException objects, while in React Native, the error parser was enhanced to extract embedded JSON payloads from Nitro NSError strings. Additionally, documentation and release notes were updated to reflect these changes. Feedback on the KMP implementation suggests using the safe call operator on NSError.userInfo to prevent a potential NullPointerException, as userInfo can be null in Kotlin/Native.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) | ||
| internal fun NSError.stringFromUserInfo(key: String): String? { | ||
| val value = userInfo[key] |
There was a problem hiding this comment.
Summary
Error.codevalues before falling back toError.messageparsing inreact-native-iap.syncIOS()cancellation surfacesuser-cancelledwithout noisy cancellation logging.NSError.userInfoOpenIAP codes inkmp-iapcompletion paths so KMP callers receive typedPurchaseException.error.codevalues.react-native-iap15.3.6 andkmp-iap2.3.6, and clarifyUserCancelledhandling in the error docs.Closes #202
Cross-SDK check
expo-iap: no matching Nitro string parser path; iOS throwsIapExceptionwithcodefrom serializedPurchaseError.flutter_inapp_purchase: no matching string parser path; iOS sendsFlutterError(code: purchaseError.code.rawValue, ...), Dart mapsPlatformException.codeincludinguser-cancelled.maui-iap: already mapsNSError.userInfo[code/message/productId/debugMessage]into typed purchase errors.godot-iap: emits typed purchase error dictionaries/JSON from native code; no matching NSError string parsing path.Release notes
Test plan
yarn jest src/__tests__/utils/error.test.ts src/__tests__/index.test.ts --runInBandyarn typecheckyarn lint./gradlew :library:iosSimulatorArm64Test --no-daemon --stacktraceinlibraries/kmp-iap./gradlew :library:build --no-daemon --stacktraceinlibraries/kmp-iapcd packages/docs && bunx prettier --check "src/**/*.{ts,tsx,js,jsx,css,json}"cd packages/docs && bun run lintcd packages/docs && bun run typecheckcd packages/docs && bun run buildbun run audit:docsgit diff --check