Skip to content

fix(ios): emit error on duplicate purchase instead of silent hang#3177

Merged
hyochan merged 4 commits intomainfrom
fix/duplicate-purchase-silent-hang
Mar 26, 2026
Merged

fix(ios): emit error on duplicate purchase instead of silent hang#3177
hyochan merged 4 commits intomainfrom
fix/duplicate-purchase-silent-hang

Conversation

@hyochan
Copy link
Copy Markdown
Owner

@hyochan hyochan commented Mar 26, 2026

Summary

  • Fixes duplicate purchase detection silently dropping events, causing the app to hang indefinitely
  • Adds ErrorCode.DuplicatePurchase ('duplicate-purchase') so apps can detect and recover from this scenario
  • Adds isDuplicatePurchaseError() public helper for convenient error checking

Closes #3176

Changes

iOS Native (ios/HybridRnIap.swift)

  • When a duplicate purchase event is detected in sendPurchaseUpdate, now calls sendPurchaseError with code duplicate-purchase instead of silently returning
  • The onPurchaseError callback now fires, allowing apps to respond (e.g., trigger restorePurchases)

TypeScript Types (src/types.ts)

  • Added DuplicatePurchase = 'duplicate-purchase' to ErrorCode enum

Error Utilities (src/utils/error.ts)

  • Added isDuplicatePurchaseError() public helper (exported via react-native-iap)

Error Mapping (src/utils/errorMapping.ts)

  • Added DuplicatePurchase to COMMON_ERROR_CODE_MAP
  • Added user-friendly message: "This purchase has already been processed. Try restoring purchases."
  • Added DuplicatePurchase to isRecoverableError list (recoverable via restorePurchases)
  • Added internal isDuplicatePurchaseError() helper

Usage

import { useIAP, isDuplicatePurchaseError } from 'react-native-iap';

const { requestPurchase, restorePurchases } = useIAP({
  onPurchaseError: (error) => {
    if (isDuplicatePurchaseError(error)) {
      restorePurchases();
      return;
    }
    // handle other errors
  },
});

Test plan

  • yarn typecheck passes
  • yarn lint passes
  • yarn test passes (260 tests, 12 suites)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Duplicate purchase attempts are now detected and reported as a distinct, recoverable error and surface a clear, user-friendly message guiding recovery (restore purchases or review options).
  • New Features

    • Exposed utilities to detect duplicate-purchase errors so UI can handle them consistently and provide targeted recovery guidance.
  • Tests

    • Added tests verifying duplicate-purchase detection, recoverability, and the user-facing message.

When iOS detects a duplicate purchase event, it previously logged a
warning and returned silently, causing neither onPurchaseSuccess nor
onPurchaseError to fire. This left the app in an infinite loading state
with no way to recover.

Now sends a 'duplicate-purchase' error via onPurchaseError so apps can
detect this scenario and trigger restorePurchases or show recovery UI.

Closes #3176

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Duplicate iOS purchase updates now emit a structured duplicate-purchase error to listeners instead of silently returning; JS utilities export a duplicate-purchase code and predicate and map it as a recoverable, user-facing error.

Changes

Cohort / File(s) Summary
iOS Duplicate Purchase Emission
ios/HybridRnIap.swift
When a duplicate purchase is detected in sendPurchaseUpdate, the code now constructs a NitroPurchaseResult with code: "duplicate-purchase", emits it via sendPurchaseError(..., productId: purchase.productId), and returns. Adds a duplicatePurchaseCode constant.
Error mapping & helpers
src/utils/errorMapping.ts, src/utils/error.ts
Adds DUPLICATE_PURCHASE_CODE = 'duplicate-purchase', isDuplicatePurchaseError(error); marks the code as recoverable in isRecoverableError and returns a specific user-facing message in getUserFriendlyErrorMessage. Re-exports helper and constant from src/utils/error.ts.
Tests
src/__tests__/utils/error.test.ts, src/__tests__/utils/errorMapping.test.ts, src/utils/__tests__/errorMapping.test.ts
Adds tests for isDuplicatePurchaseError and DUPLICATE_PURCHASE_CODE, updates recoverable/error-message expectations to include the duplicate-purchase case.

Sequence Diagram(s)

sequenceDiagram
  participant App as "App (JS)"
  participant Native as "HybridRnIap (iOS)"
  participant Listener as "JS Listener / Handler"

  App->>Native: requestPurchase / purchase flow
  Native->>Native: receive purchase update
  alt duplicate detected
    Native->>Listener: emit NitroPurchaseResult(code: "duplicate-purchase") via sendPurchaseError
    Listener->>Listener: normalize error (errorMapping)
    Listener->>App: call onPurchaseError / recoverable message ("This purchase has already been processed. Try restoring purchases.")
  else new/valid purchase
    Native->>Listener: emit success purchase result
    Listener->>App: call onPurchaseSuccess
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

Poem

🐰 I sniffed a double hop and heard a skip,
A tiny pause that froze the chip.
I gave it a name — "duplicate-purchase" bright,
Now errors wake and guide the flight.
Hoppity-hop, the app sleeps light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: converting silent handling of duplicate purchases to explicit error emission on iOS.
Linked Issues check ✅ Passed All coding requirements from issue #3176 are met: duplicate purchases now emit error via sendPurchaseError, error code and helper functions are exported, error is marked recoverable, and user-friendly messaging is provided.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving issue #3176: iOS error emission, error code constants, helper functions, error mapping, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/duplicate-purchase-silent-hang

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.72%. Comparing base (267c8b2) to head (bbe7d60).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3177      +/-   ##
==========================================
+ Coverage   69.64%   69.72%   +0.08%     
==========================================
  Files           9        9              
  Lines        1825     1830       +5     
  Branches      596      597       +1     
==========================================
+ Hits         1271     1276       +5     
  Misses        549      549              
  Partials        5        5              
Flag Coverage Δ
library 69.72% <100.00%> (+0.08%) ⬆️

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

Files with missing lines Coverage Δ
src/utils/error.ts 100.00% <ø> (ø)
src/utils/errorMapping.ts 70.85% <100.00%> (+0.75%) ⬆️
🚀 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
Copy Markdown
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 new DuplicatePurchase error code and implements logic to handle duplicate purchase events on iOS and in the TypeScript utility layers. The changes include emitting a specific error when duplicates are detected, adding the error to the ErrorCode enum, and updating error mapping and recovery logic. Review feedback highlights a redundant and inconsistent implementation of the isDuplicatePurchaseError utility across two files and suggests using a constant instead of a magic string in the iOS implementation.

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/types.ts`:
- Line 330: The DuplicatePurchase enum member was added manually to src/types.ts
but this file is generated; remove the manual edit and instead add the new enum
value to the source schema/spec that drives generation (the enum that produces
DuplicatePurchase), then run the generation command (yarn generate:types) and
commit the regenerated outputs; ensure the change touches the authoritative
schema/spec (not src/types.ts) and verify CI passes with the regenerated file
containing DuplicatePurchase = 'duplicate-purchase'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7ddf224-b241-448e-ab1a-84364d828aa4

📥 Commits

Reviewing files that changed from the base of the PR and between 642ba92 and f989bd1.

📒 Files selected for processing (4)
  • ios/HybridRnIap.swift
  • src/types.ts
  • src/utils/error.ts
  • src/utils/errorMapping.ts

hyochan and others added 2 commits March 27, 2026 06:04
Move DuplicatePurchase out of auto-generated src/types.ts and define
it as DUPLICATE_PURCHASE_CODE in errorMapping.ts. The CI step
"Ensure generated types are up to date" regenerates types.ts from
OpenIAP upstream, so local additions cause a diff failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove duplicate isDuplicatePurchaseError from error.ts; re-export
  the errorMapping.ts version which uses normalizing extractCode
- Define static duplicatePurchaseCode constant in HybridRnIap.swift
  to eliminate the magic string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hyochan hyochan added 📱 iOS Related to iOS 🛠 bugfix All kinds of bug fixes labels Mar 26, 2026
…E_CODE

Add tests across all three error test files:
- isDuplicatePurchaseError with various input formats
- DUPLICATE_PURCHASE_CODE constant value
- getUserFriendlyErrorMessage for duplicate-purchase
- isRecoverableError includes duplicate-purchase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
src/utils/__tests__/errorMapping.test.ts (1)

155-166: Consider adding a string argument test for consistency.

The isUserCancelledError tests include string-only argument cases (lines 149-150). If isDuplicatePurchaseError supports the same signature, consider adding a similar test:

expect(isDuplicatePurchaseError('duplicate-purchase')).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/__tests__/errorMapping.test.ts` around lines 155 - 166, Add a
string-argument test for isDuplicatePurchaseError to mirror
isUserCancelledError's coverage: update the test block for
isDuplicatePurchaseError to include an assertion like
expect(isDuplicatePurchaseError('duplicate-purchase')).toBe(true), keeping the
existing object-argument assertions (e.g., {code: DUPLICATE_PURCHASE_CODE})
intact so both string and object signatures are validated for the
isDuplicatePurchaseError function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/__tests__/errorMapping.test.ts`:
- Around line 155-166: Add a string-argument test for isDuplicatePurchaseError
to mirror isUserCancelledError's coverage: update the test block for
isDuplicatePurchaseError to include an assertion like
expect(isDuplicatePurchaseError('duplicate-purchase')).toBe(true), keeping the
existing object-argument assertions (e.g., {code: DUPLICATE_PURCHASE_CODE})
intact so both string and object signatures are validated for the
isDuplicatePurchaseError function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5abc0f75-241a-402d-8864-68a64ec745b0

📥 Commits

Reviewing files that changed from the base of the PR and between b13bd2b and bbe7d60.

📒 Files selected for processing (3)
  • src/__tests__/utils/error.test.ts
  • src/__tests__/utils/errorMapping.test.ts
  • src/utils/__tests__/errorMapping.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/tests/utils/error.test.ts

@hyochan hyochan merged commit 2882e58 into main Mar 26, 2026
10 checks passed
@hyochan hyochan deleted the fix/duplicate-purchase-silent-hang branch March 26, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠 bugfix All kinds of bug fixes 📱 iOS Related to iOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Duplicate purchase update skipped" is not an error, so app just hangs

1 participant