Skip to content

Bump PHC to 19.3.0 and fix errors not matching PurchasesError - #579

Merged
AlvaroBrey merged 6 commits into
mainfrom
fix/error-unwrapping-issue-302
Sep 21, 2026
Merged

AlvaroBrey merged 6 commits into
mainfrom
fix/error-unwrapping-issue-302

Conversation

@vegaro

@vegaro vegaro commented Oct 29, 2025 •

Copy link
Copy Markdown
Member
  • Errors rejected by the native plugin now match the PurchasesError interface on both platforms.
  • Every rejection from Purchases runs through normalizePurchasesError from purchases-hybrid-common, inside the Proxy src/index.ts already had for trackCustomPaywallImpression. addListener keeps the remove property Capacitor attaches to its promise.
  • userCancelled reads false for every SDK error and true for a cancelled purchase, derived from code (SDK-4448). Released versions never had the field at the top level at all.
  • Carries the purchases-hybrid-common 19.3.0 bump, superseding [AUTOMATIC BUMP] Updates purchases-hybrid-common to 19.3.0 #918.
  • Adds jest plus a run-unit-tests CircleCI job for it, and puts that job in the release tag gate alongside the existing test jobs.

Closes #302. Tracked in SDK-4450 and SDK-4448.

  • A description about what and why you are contributing, even if it's trivial.

  • The issue number(s) or PR number(s) in the description if you are contributing in response to those.

  • If applicable, unit tests.

Agent description

Motivation

#302 has reported since 2024 that rejected errors do not match the PurchasesError interface, most visibly on iOS. On Android, Capacitor puts whatever the bridge passes as the third argument of reject under a data property, so the payload was there but in the wrong place. On iOS the bridge never passed the payload, so consumers got only code and message.

Description

  • rejectWithErrorContainer in PluginHelperExtensions.swift passes error.info as the data argument of call.reject, so iOS rejections carry the same nested payload Android always had. Both bridges wrap that argument under data themselves.
  • src/index.ts wraps every function on the registered plugin. A rejected promise is chained through normalizePurchasesError, which lifts the payload to the top level, sets userInfo to the whole payload, and derives userCancelled from code. It re-attaches remove, which addListener puts on its promise for the deprecated non-awaited call style and a plain .then would drop.
  • Web is unaffected: PurchasesWeb rejects plain strings, and the normalizer only touches objects whose code is numeric.
  • Adds jest with a suite pinning the exact error shape a consumer sees, including the legacy data key, and a run-unit-tests CircleCI job.

Before and after

Captured with an invalid API key. Before, on Android the five fields sat under data only; on iOS the error carried nothing but code, errorMessage and message. After, on both platforms, pinned by the test:

{
  "code": "11",
  "message": "There was a credentials issue. Check the underlying error for more details.",
  "readableErrorCode": "InvalidCredentialsError",
  "underlyingErrorMessage": "Invalid API Key.",
  "userCancelled": false,
  "userInfo": { ...the five keys below... },
  "data": {
    "code": 11,
    "message": "There was a credentials issue. Check the underlying error for more details.",
    "readableErrorCode": "InvalidCredentialsError",
    "readable_error_code": "InvalidCredentialsError",
    "underlyingErrorMessage": "Invalid API Key."
  }
}

iOS reports INVALID_CREDENTIALS where Android reports InvalidCredentialsError; the readable code names differ per platform today, tracked in SDK-4464.

Captured by hand on a Pixel emulator on 2026-09-10 and on an iPhone 17 simulator on 2026-09-18, running the example app against a bogus API key. Both platforms produce the same object: one data level with the five keys, code as a string, userInfo holding the whole payload, and userCancelled: false. iOS still reports INVALID_CREDENTIALS where Android reports InvalidCredentialsError, and its message appends the underlying message, both of which predate this PR.

Limitations:

  • data stays on the error for backwards compatibility. Nothing removes it.
  • Android NOT_SUPPORTED rejections use a string code and are not normalized. Pre-existing.

Rejected:

  • Shipping the wrapper from purchases-hybrid-common as withNormalizedErrors, which an earlier version of this stack did. It put Capacitor's remove handling and React Native's promise constraints into shared code and turned that package into a runtime hook rather than a set of contracts.
  • Normalizing per method: the proxy already existed and covers every method, so a per-method list would only add something to keep in sync.

Note

Medium Risk
Changes how all native rejections surface to app code (including userCancelled) and touches the iOS bridge error path; behavior is covered by new unit tests but affects every failed Purchases call.

Overview
Fixes rejected native plugin errors so they match the documented PurchasesError shape (closes #302) and bumps purchases-hybrid-common to 19.3.0.

On the JS side, src/index.ts generalizes the existing Proxy: every plugin method is wrapped so rejections run through normalizePurchasesError, while addListener still keeps Capacitor’s remove on the returned promise and trackCustomPaywallImpression still maps offering options for the bridge. On iOS, rejectWithErrorContainer now passes error.info into call.reject so the bridge carries the same payload Android already nested under data.

Jest is added (yarn test, jest.config.js) with tests for error shape and Capacitor method/promise parity. CircleCI runs a new run-unit-tests job on all branches and requires it before release tagging.

Reviewed by Cursor Bugbot for commit d57e714. Bugbot is set up for automated code reviews on this repo. Configure here.

@vegaro
vegaro requested a review from a team as a code owner October 29, 2025 14:54
@vegaro vegaro added the pr:fix label Oct 29, 2025

@ajpallares ajpallares left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it makes sense!

Should we add some unit tests for this?

Also, I'll leave approval to someone with more ts knowledge 🙏

tonidero
tonidero previously approved these changes Oct 30, 2025

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a question, but if we've tested it with most functions on the SDK in both Android and iOS, it does make sense. Thank you so much for handling this!!

Comment thread src/index.ts Outdated
target: any,
methodName: string | symbol
): T {
const wrappedFn = function(this: any, ...args: any[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we could apply a fix here so that, if any parameters are of type Vue.js reference (or maybe use something as a proxy, like the presence of a toRaw function, use that. In order to avoid these issues we've had with Vue.js reactivity... But in any case, that can come in a separate PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I have another branch for that, but since I am not quite sure how to test it. It's in another branch. Just opened the draft PR #583

Comment thread src/index.ts Outdated
const value = Reflect.get(target, prop, receiver);

if (typeof value === 'function') {
if (typeof prop === 'number') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm I'm wondering is this ever the case? If i'm not wrong here, prop is the function name right?

@vegaro vegaro Nov 3, 2025 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this can occur if a function gets called with index access [0], but I will double check

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we might be able to skip this tbh

@vegaro

vegaro commented Jan 12, 2026

Copy link
Copy Markdown
Member Author

I should check this still works fine with Capacitor 8 before merging

@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch from f42ff55 to 916bdec Compare August 24, 2026 13:53
@AlvaroBrey
AlvaroBrey requested a review from a team as a code owner August 24, 2026 13:53
@AlvaroBrey
AlvaroBrey marked this pull request as draft August 24, 2026 13:53

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread package.json Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch 2 times, most recently from 723b4e1 to a42bc2c Compare September 17, 2026 16:31
AlvaroBrey added a commit to RevenueCat/purchases-hybrid-common that referenced this pull request Sep 17, 2026
…1838)

- Adds `normalizePurchasesError`, so errors reaching JS consumers match
the `PurchasesError` interface this package declares. Before this change
they did not meet the interface, on either React Native or Capacitor:
- React Native: the bridge forwards only `NSError.userInfo` to JS, so on
iOS the `ErrorContainer.info` payload never arrived at all, and on
Android it arrives nested under `userInfo`. `readableErrorCode`,
`underlyingErrorMessage` and `userCancelled` are declared at the top
level but were never there on either platform.
- Capacitor: Android puts the payload under `data`; iOS passed nothing,
so the same three fields were `undefined` and only `code` and `message`
existed. Reported as RevenueCat/purchases-capacitor#302, open since v7.
- iOS: `ErrorContainer.error` is now the original `NSError` rather than
a re-wrapped copy with `readableErrorCode` injected into its `userInfo`.
Only `react-native-purchases` ever read that key, and it now does that
merge at its own reject site in RevenueCat/react-native-purchases#1919.
`flutter`, `cordova` and `unity` read `info` and are unaffected.
- Builds on #1635, which makes the web
mapper emit a string `code`. **Must merge together with
RevenueCat/purchases-capacitor#579 and
RevenueCat/react-native-purchases#1919**
- `userCancelled` is derived from `code` for every normalized error
(SDK-4448). React Native's eight `.catch` blocks that re-derived it are
removed in #1919, so it is present on every SDK error: `true` for a
cancelled purchase on every platform (Capacitor and RN web never
reported it), `false` elsewhere, where it was absent on Capacitor and on
RN's non-purchase methods.



Part of react-native-purchases#1756 and purchases-capacitor#302. Tracked
in SDK-4450 and SDK-4448.

### Checklist

- [x] A description about what and why you are contributing, even if
it's trivial.

- [x] The issue number(s) or PR number(s) in the description if you are
contributing in response to those.

  - [x] If applicable, unit tests.

<details><summary>Agent description</summary>

### Motivation

This package builds a consistent error payload in `ErrorContainer.info`,
but it does not own how that payload reaches JS. Each host framework
nests it under a different key: React Native under `userInfo`, Capacitor
under `data`. Both iOS paths dropped it entirely. Meanwhile the
`PurchasesError` interface declares every field flat at the top level, a
shape no bridge produces.

The interface has been wrong since it was copied out of
`react-native-purchases` in #455. It was already half wrong then:
react-native-purchases#268 had found in 2021 that `readableErrorCode`
actually lives in `userInfo`, added `userInfo: ErrorInfo` and deprecated
the flat field, but left `underlyingErrorMessage` and `userCancelled`
with the identical defect.

Reported downstream as RevenueCat/purchases-capacitor#302, open since
v7.

### Description

- `normalizePurchasesError` reads the payload from `userInfo` or `data`
and lifts the declared fields to the top level. It mutates the error in
place and returns it: copying into a new object would discard the
prototype and stack, breaking `instanceof Error` for consumers and
`instanceof CapacitorException` on Capacitor.
- It only acts on errors whose `code` is numeric. Plugin level
rejections use names such as `UNIMPLEMENTED` or `PAYWALL_ERROR` and are
left untouched.
- `userInfo` is merged rather than replaced, so Capacitor gains the full
payload it was missing while React Native keeps the richer one its
bridge already provides. Both platforms now expose the same `userInfo`.
- Only the normalizer lives here. Each SDK wraps its own native module
and calls it on every rejection; the interception is bridge specific
(React Native must never copy a TurboModule promise, Capacitor must keep
`addListener().remove`), so it belongs with the bridge.
- iOS: `ErrorContainer` no longer rebuilds the `NSError`. The long
standing `readableErrorCode` special case goes with it.

**Not visible in the diff:** the JS normalizer is exercised against hand
written fixtures of each bridge envelope, taken from reading the
bridges. Checked once by hand on 2026-09-10 against an iPhone 17
simulator, a Pixel emulator (React Native) and a Pixel emulator
(Capacitor) with a bogus API key: every declared field was present with
the declared type on all three, and the captured React Native iOS
`userInfo` was a superset of the fixture. Nothing repeats that check
automatically, which is the same gap that let the original mismatch
persist. `react-native-purchases` has no iOS test target, so the merge
it now performs is compile checked there; `ErrorContainerPayloadTests`
here pins that merge's input across the three ways this package builds
an `NSError`.

**Regression gate:** `errorNormalizer.test.ts` "satisfies
PurchasesError" across the per bridge fixtures, which is the contract
the consumers' own wrappers rely on. The Hermes constraint that shaped
the React Native wrapper (enumerating a TurboModule promise's own keys
throws `Cannot read property 'length' of null`) is documented and tested
in react-native-purchases#1919, where that wrapper now lives.

**Limitations:**

- Web errors still get an empty `readableErrorCode`, because
`purchases-js-hybrid-mappings`' `mapPurchasesError` never emits one.
That mapper is the right place to fix it.
- Web `userInfo` carries `statusCode` and `backendErrorCode` lifted from
`info`, but not `userCancelled`, which the mapper puts at the top level.
Native `info` includes it for purchase errors.
- `userCancelled` is `boolean | null` in the interface but never `null`
at runtime any more. Narrowing the type waits for a major.
- Only `react-native-purchases` and `purchases-capacitor` consume this
package. `flutter`, `cordova` and `unity` forward `info` faithfully and
need no changes.

**Rejected:**

- Matching `code` against `PURCHASES_ERROR_CODE` membership. That enum
omits codes the native SDKs emit (36 to 41), and Android and iOS
disagree on 28 and 36, so it would reject genuine errors, including the
web purchase redemption ones.
- Shipping the module wrapper from here as `withNormalizedErrors`. It
was one place to test, but it encoded both bridges' quirks in shared
code and made this package a runtime hook rather than a contract; an
earlier version of this PR did that.
- Keeping the iOS merge here. It exists only for React Native's bridge,
and leaving it gave the other four hybrids a `userInfo` they never asked
for while making Capacitor send the payload twice.
- Flattening `info` onto the error in native code, matching `cordova`
and `unity`. No bridge lets native add top level keys.
- Guarding the code coercion so web kept its numeric `code` until the
next major. It left react-native-purchases#1756 unfixed, and #1635
already stringifies at the source, so the guard would have been dead
code.
- Making the phantom interface fields optional. Honest, but it breaks
compilation for anyone assigning them to a non-optional `string`.
Growing the runtime gets the same honesty additively.

</details>



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes cross-platform error shape and iOS ErrorContainer forwarding;
consumers must call the normalizer on rejections (coordinated PRs), but
behavior is covered by new unit tests.
> 
> **Overview**
> Adds **`normalizePurchasesError`** to the TypeScript package so hybrid
SDKs can lift bridge-nested payloads (`userInfo`, `data`, `info`) onto a
flat **`PurchasesError`** shape in place—preserving prototypes/stacks
and only touching numeric SDK codes. Jest fixtures cover React Native,
Capacitor, and web envelopes; CI gains a **`typescript-tests`** job and
lint runs via **`yarn lint`**.
> 
> On iOS, **`ErrorContainer`** no longer re-wraps the **`NSError`** to
inject **`readableErrorCode`** into **`userInfo`**; it forwards the
original error and still fills **`info`** (including a
**`rc_code_name`** fallback when **`readable_error_code`** is missing).
New Swift tests lock the hybrid **`info`** contract.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b8259d1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch from 6381942 to 25a36ad Compare September 18, 2026 09:32
@AlvaroBrey AlvaroBrey changed the title Fix errors not matching PurchasesError Bump PHC to 19.3.0 and fix errors not matching PurchasesError Sep 18, 2026
@AlvaroBrey

Copy link
Copy Markdown
Member

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 25a36ad. Configure here.

@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch from 0f51ee4 to be2d40e Compare September 18, 2026 11:01
@AlvaroBrey
AlvaroBrey added this pull request to stack #920 September 18, 2026 14:02
@AlvaroBrey
AlvaroBrey marked this pull request as ready for review September 18, 2026 14:02
@AlvaroBrey
AlvaroBrey dismissed tonidero’s stale review September 18, 2026 14:05

Dismissing: the branch has been rewritten since this approval and none of the current content was reviewed.

Supersedes purchases-capacitor#918.
call.reject dropped the error container's info dictionary, so iOS
rejections reached JS with only a code and a message. Android has always
received the same payload under `data`, which Capacitor fills from this
argument.
readableErrorCode, underlyingErrorMessage and userCancelled are declared
on PurchasesError but only ever arrived nested under `data`. The proxy
src/index.ts already had for trackCustomPaywallImpression now runs
normalizePurchasesError on every rejection, and re-attaches the `remove`
property addListener puts on its promise, which chaining would drop.
Adds jest and a CircleCI job for it. The suite pins the exact error
shape a consumer sees, including the legacy `data` key, and that
addListener keeps `remove`.
tag-current-branch did not list run-unit-tests, so on release branches
the tag could be pushed before the tests finished or after they failed.
@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch from be2d40e to c9639d4 Compare September 21, 2026 07:58
@AlvaroBrey

Copy link
Copy Markdown
Member

@ajpallares and @tonidero , I'm requesting re-review as I've taken over this PR and reworked it to match RevenueCat/react-native-purchases#1919

PHC now provides normalizeErrorCodes which is what this and RN use for fixing the shape.

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a small concern... But don't consider it a blocker. Thanks for doing this!!

Comment thread src/index.ts

@ajpallares ajpallares left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it makes sense!

Comment thread .circleci/config.yml
@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch 5 times, most recently from 5f3a89f to 3269691 Compare September 21, 2026 10:46
@AlvaroBrey
AlvaroBrey force-pushed the fix/error-unwrapping-issue-302 branch from 3269691 to d57e714 Compare September 21, 2026 10:49
@AlvaroBrey
AlvaroBrey merged commit 7f3efc7 into main Sep 21, 2026
18 checks passed
@AlvaroBrey
AlvaroBrey deleted the fix/error-unwrapping-issue-302 branch September 21, 2026 12:42
AlvaroBrey added a commit that referenced this pull request Sep 21, 2026
)

- Adds an iOS test target. The package had none, so every Swift change
in this repo has only ever been compile checked.
- Pins the shape `rejectWithErrorContainer` hands the bridge: the
payload must sit under exactly one `data` level. `CAPPluginCallError`
wraps the reject `data` argument itself, so passing an already wrapped
payload buries it one level below where the JS layer reads it, and every
declared field comes back empty.
- Runs in the existing `run-tests-ios` job rather than a new one, which
avoids a second mac executor and needs no change to the release gate,
since that job is already in `tag-current-branch`'s `requires`.
- Stacked on #579. Merge that one first.

- [x] A description about what and why you are contributing, even if
it's trivial.

- [x] The issue number(s) or PR number(s) in the description if you are
contributing in response to those.

  - [x] If applicable, unit tests.

<details><summary>Agent description</summary>

### Motivation

#579 changes what iOS passes to `call.reject`, which is the one thing in
that PR no test could see. A commit on it buried the payload an extra
level and reached CI green, because the jest suite builds its own
envelope and so exercises the normalizer rather than the shape this
bridge produces. Capacitor's two bridges differ in how they treat that
argument, and the only evidence either way was reading Capacitor's own
source. That reading went wrong once already and produced a regression
that CI happily passed.

### Description

- `Package.swift` gains a `.testTarget` depending on the existing target
plus `Capacitor` and `PurchasesHybridCommon`, at
`ios/Tests/RevenuecatPurchasesCapacitorTests`. The podspec glob is
`ios/Sources/**`, so nothing here reaches CocoaPods consumers.
- `PluginHelperExtensionsTests` builds a `CAPPluginCall` with its own
handlers, captures the `CAPPluginCallError` that
`rejectWithErrorContainer` produces, and asserts the payload sits under
one `data` level with the code, message and readable code inside it. A
second spec asserts the message and code travel outside that payload.
- `yarn test:ios` runs it, wired into `run-tests-ios`.

**Not visible in the diff:** the fixture builds a plain `NSError` in the
RevenueCat domain carrying `readable_error_code` rather than using
`ErrorCode`. purchases-hybrid-common exports only its own two libraries,
so `RevenueCat` is not importable here, and adding a second pin of
purchases-ios for tests alone would be worse than the problem. The error
still exercises the same `ErrorContainer` derivation.

**Regression gate:** `testPayloadSitsUnderExactlyOneDataLevel`.
Reintroducing the extra nesting fails it with "the payload is nested
twice" and leaves the other spec passing, checked locally on an iPhone
17 simulator.

**Limitations:** this pins what we hand Capacitor, not the final JS
object. The serialization that turns it into a `CapacitorException`
lives inside Capacitor and is internal to that module. The end to end
shape was confirmed by hand on a simulator for #579; covering it
automatically would need the maestro e2e flow.

</details>


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test and CI wiring only; no production Swift or JS runtime behavior
changes in this diff.
> 
> **Overview**
> Adds the repo’s first **native iOS unit tests** so Swift bridge
behavior is checked beyond compile-only `verify:ios`.
> 
> `Package.swift` defines a `.testTarget` at
`ios/Tests/RevenuecatPurchasesCapacitorTests`. New specs exercise
`rejectWithErrorContainer` and assert the Capacitor reject envelope
keeps error fields under **exactly one** `data` level (avoiding
double-wrapping that breaks JS), with code/message/readableErrorCode in
the payload and message/code on the outer rejection.
> 
> **`yarn test:ios`** runs `xcodebuild test` on an iPhone 17 simulator;
CircleCI **`run-tests-ios`** invokes it after the existing iOS verify
step, so release gates that already depend on that job pick up the tests
without a new job.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
314b609. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
RCGitBot added a commit that referenced this pull request Sep 24, 2026
**This is an automatic release.**

## RevenueCat SDK
### 🐞 Bugfixes
* Bump PHC to 19.3.0 and fix errors not matching PurchasesError (#579)
via Cesar de la Vega (@vegaro)
### 📦 Dependency Updates
* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 19.3.1 (#926) via
RevenueCat Git Bot (@RCGitBot)
* [Android
10.22.1](https://github.com/RevenueCat/purchases-android/releases/tag/10.22.1)
* [Android
10.22.0](https://github.com/RevenueCat/purchases-android/releases/tag/10.22.0)
* [iOS
5.90.2](https://github.com/RevenueCat/purchases-ios/releases/tag/5.90.2)
* [iOS
5.90.1](https://github.com/RevenueCat/purchases-ios/releases/tag/5.90.1)
* [iOS
5.90.0](https://github.com/RevenueCat/purchases-ios/releases/tag/5.90.0)
* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 19.2.0 (#916) via
RevenueCat Git Bot (@RCGitBot)

### 🔄 Other Changes
* Bump fastlane-plugin-revenuecat_internal from `fc64a1a` to `9f7a03e`
(#925) via dependabot[bot] (@dependabot[bot])
* Auto-merge PHC bump PRs (#924) via Álvaro Brey (@AlvaroBrey)
* test(ios): Add an iOS test target pinning the reject payload shape
(#919) via Álvaro Brey (@AlvaroBrey)
* Bump fastlane from 2.240.0 to 2.240.1 (#922) via dependabot[bot]
(@dependabot[bot])
* Bump fastlane-plugin-revenuecat_internal from `6db1da0` to `fc64a1a`
(#923) via dependabot[bot] (@dependabot[bot])
* Bump fastlane from 2.239.0 to 2.240.0 (#917) via dependabot[bot]
(@dependabot[bot])
* ci: approve the release hold automatically when the release PR is
approved (#912) via Álvaro Brey (@AlvaroBrey)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Patch release pulls newer PHC and native billing SDKs and fixes
purchase error typing, which can affect failure handling in purchase
flows.
> 
> **Overview**
> **Automatic patch release** that publishes **13.6.1** for
`@revenuecat/purchases-capacitor` and
`@revenuecat/purchases-capacitor-ui`, including native plugin version
strings on Android and iOS.
> 
> The release notes call out a **bugfix** from updated
**purchases-hybrid-common** (19.3.x): native reject payloads should
align with **`PurchasesError`** again (#579). **`VERSIONS.md`**
documents the new stack—PHC **19.3.1**, iOS **5.90.2**, Android
**10.22.1**.
> 
> In this PR diff, runtime code changes are limited to **version
constant bumps**; **`CHANGELOG.md`**, **`CHANGELOG.latest.md`**, and
**`.version`** are refreshed for the release.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a53bd15. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PurchasesError actual data doesn't match interface

4 participants