@entitlehub/react-native 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,38 +1,86 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.7 2026-07-22
3
+ ## 0.1.9 (2026-09-20)
4
+
5
+ ### Fixed
6
+ - **`restorePurchases()` swallowed every per-purchase failure.** Each purchase was reported with
7
+ `.catch(() => {})`, so a `409 receipt_revoked` (the signal that a purchase was REFUNDED) died
8
+ inside the SDK and no amount of catching in the app could see it. A restore is the main place a
9
+ refund surfaces, so this was the path that mattered most. Failures now reach an optional
10
+ `onReportError`, and `console.warn` otherwise.
11
+
12
+ ### Read this before upgrading
13
+ - **If you were catching around `restorePurchases()` to detect a refund, that never worked and
14
+ still will not.** The loop deliberately continues past a bad receipt so one stale purchase can't
15
+ cost someone the rest of theirs, which means `restorePurchases()` **resolves** rather than
16
+ rejecting. Pass `onReportError` - it is the only way to see these:
17
+
18
+ ```ts
19
+ import { restorePurchases, isReceiptRevoked } from "@entitlehub/react-native";
20
+
21
+ const info = await restorePurchases({
22
+ onReportError: (err) => { if (isReceiptRevoked(err)) showRefundNotice(); },
23
+ });
24
+ ```
25
+
26
+ Passing the options object to **0.1.7/0.1.8 is harmless** (JS drops the extra argument), so it is
27
+ safe to add before you upgrade.
28
+ - `isReceiptRevoked` is re-exported here, so you don't need to import `@entitlehub/sdk` for it.
29
+ It is a **0.3.1** symbol, so the peer range moves to `>=0.3.1`. On an older core SDK the
30
+ re-export resolves to `undefined` rather than failing loudly, which is the whole reason the
31
+ range had to move: `>=0.2.0` would have permitted a version that cannot satisfy it.
32
+
33
+ ## 0.1.8 (2026-09-19)
34
+
35
+ ### Fixed
36
+ - **Black screen on launch: "Requiring unknown module expo-iap".** The SDK loaded `expo-iap` with
37
+ `require()`. Metro decides what to bundle by reading literal `require("…")` / `import("…")` calls,
38
+ and the ESM build rewrote that call into esbuild's `__require` shim, which Metro cannot see. So
39
+ `expo-iap` was never put in the bundle, the first call threw, and React Native tore the UI down
40
+ with it, before the store sheet ever opened. It is now loaded with `import("expo-iap")`, which
41
+ survives the build as a literal. Verified by bundling a real Expo app: with 0.1.7 the store
42
+ library is absent from the bundle, with 0.1.8 it is there.
43
+ - The module now loads while `configureEntitleHub()` runs, so a missing or broken `expo-iap` raises
44
+ a clear error there instead of being swallowed by the store-connection catch, and handles CJS
45
+ interop (module namespace or `.default`).
46
+
47
+ ### Changed
48
+ - `configureEntitleHub({ iap })` is no longer a workaround for the bug above, just the way to pin an
49
+ exact module instance or use another OpenIAP-compatible library.
50
+
51
+ ## 0.1.7 (2026-07-22)
4
52
 
5
53
  ### Fixed
6
54
  - **A completed purchase could still be reported as failed.** After a timeout, `purchaseProduct()`
7
55
  checked for confirmation exactly once. Store notifications are asynchronous, so that single
8
- reading races the grant and sometimes loses observed live: the entitlement was written seconds
56
+ reading races the grant and sometimes loses, observed live: the entitlement was written seconds
9
57
  after the check ran, and the user saw a purchase-failed error for a purchase that worked. It now
10
58
  polls the store and the server with backoff (`reconcileMs`, default 30s) before giving up.
11
59
  - **The timeout counted the user's own time.** The timer was armed when `purchaseProduct()` was
12
- called before the store's password / Face ID sheet appears so a user who paused mid-purchase
60
+ called: before the store's password / Face ID sheet appears, so a user who paused mid-purchase
13
61
  tripped it. It now starts once the request is actually in flight, with a separate generous
14
62
  backstop (`sheetTimeoutMs`, default 10m) so a stuck bridge still can't hang forever.
15
63
 
16
64
  ### Changed
17
- - **Rejections are now typed `PurchaseError`s branch on `.code`, not the message.**
65
+ - **Rejections are now typed `PurchaseError`s: branch on `.code`, not the message.**
18
66
  `purchase-cancelled`, `purchase-failed`, and the new `purchase-pending`. **`purchase-pending` is
19
- not a failure**: the store took the money but hasn't confirmed yet. Show "still confirming" the
67
+ not a failure**: the store took the money but hasn't confirmed yet. Show "still confirming", the
20
68
  entitlement lands on its own and `addCustomerInfoUpdateListener` fires when it does. `.pending`
21
69
  and `.customerInfo` are on the error. The old untyped `"purchase-timeout"` message is gone.
22
70
 
23
- ## 0.1.6 2026-07-21
71
+ ## 0.1.6 (2026-07-21)
24
72
 
25
73
  ### Added
26
74
  - Reports `react-native/<version>` to EntitleHub on each call, so the dashboard can tell you when
27
- your SDK is behind a release with fixes in it rather than you finding out by hitting the bug.
75
+ your SDK is behind a release with fixes in it, rather than you finding out by hitting the bug.
28
76
  Version only; no device or user data.
29
77
 
30
- ## 0.1.5 2026-07-21
78
+ ## 0.1.5 (2026-07-21)
31
79
 
32
80
  ### Fixed
33
81
  - **`purchaseProduct()` could hang forever after a successful purchase.** It waited only on
34
82
  `purchaseUpdatedListener`; on the react-native-iap 15+ / Nitro bridge that callback never fires,
35
- so the promise never settled the paywall stayed open and features stayed locked even though the
83
+ so the promise never settled: the paywall stayed open and features stayed locked even though the
36
84
  user had been charged and EntitleHub had already granted the entitlement server-side.
37
85
  `requestPurchase`'s resolved value was being discarded entirely; it is now honoured, so bridges
38
86
  that resolve instead of emitting an event work.
@@ -40,14 +88,14 @@
40
88
  ### Added
41
89
  - Reconciliation safety net: if no result arrives within `timeoutMs` (default 120s),
42
90
  `purchaseProduct()` re-reads `getAvailablePurchases()` and, failing that, re-checks entitlements
43
- from the server resolving successfully if the entitlement appeared while the client was waiting.
91
+ from the server: resolving successfully if the entitlement appeared while the client was waiting.
44
92
  Only rejects (`"purchase-timeout"`) when nothing was actually granted.
45
93
  - `purchaseProduct(id, { timeoutMs })` to tune that window.
46
94
 
47
- ## 0.1.2 2026-07-18
95
+ ## 0.1.2 (2026-07-18)
48
96
 
49
97
  ### Added
50
- - `configureEntitleHub({ iap })` inject the OpenIAP-compatible store module (pinned expo-iap
98
+ - `configureEntitleHub({ iap })`: inject the OpenIAP-compatible store module (pinned expo-iap
51
99
  version, or react-native-iap) instead of the hard-coded `expo-iap`. Decouples this SDK from a
52
100
  single store-library version so you can work around a native issue without changing the SDK.
53
101
 
@@ -56,17 +104,17 @@
56
104
  thread) are a store-library-version concern; how to pin a version, inject a module, or fall back
57
105
  to the fully-decoupled `@entitlehub/sdk` + your own billing library.
58
106
 
59
- ## 0.1.1 2026-07-18
107
+ ## 0.1.1 (2026-07-18)
60
108
 
61
109
  ### Fixed
62
- - **`purchaseProduct` crashed** 0.1.0 was written against expo-iap's old flat `requestPurchase`
110
+ - **`purchaseProduct` crashed**: 0.1.0 was written against expo-iap's old flat `requestPurchase`
63
111
  API and read the purchase from the return value. `expo-iap@4.x` is **event-based** with a
64
112
  discriminated request. Now: `requestPurchase({ request: { apple: { sku }, google: { skus } },
65
113
  type })` with the result bridged from `purchaseUpdatedListener` / `purchaseErrorListener`.
66
114
  - Android subscriptions now pass the required `offerToken` (fetched from the product).
67
115
  - `getProducts` uses expo-iap's `fetchProducts` (the old `getProducts` no longer exists).
68
116
 
69
- ## 0.1.0 2026-07-18
117
+ ## 0.1.0 (2026-07-18)
70
118
 
71
119
  Initial release.
72
120
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@entitlehub/react-native)](https://www.npmjs.com/package/@entitlehub/react-native)
4
4
  [![license MIT](https://img.shields.io/npm/l/@entitlehub/react-native)](./LICENSE)
5
5
 
6
- EntitleHub for **React Native & Expo** one call to purchase and unlock entitlements. It opens the
6
+ EntitleHub for **React Native & Expo**: one call to purchase and unlock entitlements. It opens the
7
7
  native store sheet (via [`expo-iap`](https://github.com/hyochan/expo-iap)), validates the receipt
8
8
  with EntitleHub server-side, and hands you the updated entitlements. The RevenueCat developer
9
9
  experience, on EntitleHub.
@@ -26,7 +26,7 @@ await configureEntitleHub({ apiKey: "pk_live_…", appUserId: user.id });
26
26
  // gate a feature:
27
27
  if (await isEntitled("pro")) unlockPro();
28
28
 
29
- // buy opens the native sheet, validates, returns entitlements:
29
+ // buy: opens the native sheet, validates, returns entitlements:
30
30
  const info = await purchaseProduct("app_pro_monthly", { isSubscription: true });
31
31
  if (info.isActive("pro")) unlockPro();
32
32
 
@@ -45,7 +45,7 @@ const restored = await restorePurchases();
45
45
  | `addCustomerInfoUpdateListener(fn)` | React to entitlement changes. |
46
46
 
47
47
  Only your **publishable** key (`pk_`) ships in the app. EntitleHub validates the Apple/Google
48
- receipt server-side, so a forged one is rejected no secret key on the device.
48
+ receipt server-side, so a forged one is rejected, no secret key on the device.
49
49
 
50
50
  ## How it works
51
51
 
@@ -60,22 +60,25 @@ Full guide: **[entitlehub.com/docs/purchases](https://entitlehub.com/docs/purcha
60
60
  ## Choosing / pinning the store library
61
61
 
62
62
  The on-device purchase runs through **expo-iap** (OpenIAP). That native layer is the store
63
- library's responsibility, not EntitleHub's and a given version can have platform-specific native
63
+ library's responsibility, not EntitleHub's, and a given version can have platform-specific native
64
64
  bugs. If you hit a **native IAP crash** (e.g. an `EXC_BAD_ACCESS` in the store module), it's the
65
65
  store library, not this SDK or your app code, and no JS change can catch a native exception thrown
66
66
  off the JS thread.
67
67
 
68
68
  Two levers, both without changing this SDK:
69
69
 
70
- - **Pin a working version:** `npm install expo-iap@<version>` this package uses whatever `expo-iap`
70
+ - **Pin a working version:** `npm install expo-iap@<version>`, this package uses whatever `expo-iap`
71
71
  is installed (peer dependency).
72
- - **Supply your own module:** pass any OpenIAP-compatible library to `configureEntitleHub`:
72
+ - **Supply your own module:** pass any OpenIAP-compatible library to `configureEntitleHub`. You do
73
+ not need this to use `expo-iap`, which is loaded for you; it is for pinning an exact instance or
74
+ using a different library:
73
75
  ```ts
74
- await configureEntitleHub({ apiKey, appUserId, iap: require("expo-iap") }); // or react-native-iap
76
+ import * as iap from "expo-iap"; // or react-native-iap
77
+ await configureEntitleHub({ apiKey, appUserId, iap });
75
78
  ```
76
79
 
77
80
  Fully decoupled fallback: skip this package and use **[`@entitlehub/sdk`](https://www.npmjs.com/package/@entitlehub/sdk)**
78
- directly open the purchase with *any* billing library you've verified on your target OS, then call
81
+ directly: open the purchase with *any* billing library you've verified on your target OS, then call
79
82
  `eh.reportPurchase({ signedTransaction | purchaseToken })`. See
80
83
  [Purchases → manual](https://entitlehub.com/docs/purchases).
81
84
 
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -27,23 +37,34 @@ __export(index_exports, {
27
37
  getOfferings: () => getOfferings,
28
38
  getProducts: () => getProducts,
29
39
  isEntitled: () => isEntitled,
40
+ isReceiptRevoked: () => import_sdk2.isReceiptRevoked,
30
41
  logIn: () => logIn,
31
42
  purchaseProduct: () => purchaseProduct,
32
43
  restorePurchases: () => restorePurchases
33
44
  });
34
45
  module.exports = __toCommonJS(index_exports);
35
46
  var import_sdk = require("@entitlehub/sdk");
47
+ var import_sdk2 = require("@entitlehub/sdk");
36
48
  var _iap;
37
- function iap() {
49
+ async function loadIap() {
38
50
  if (_iap) return _iap;
51
+ let mod;
39
52
  try {
40
- _iap = require("expo-iap");
53
+ mod = await import("expo-iap");
41
54
  } catch {
42
55
  throw new Error("@entitlehub/react-native needs 'expo-iap'. Install it with: npx expo install expo-iap");
43
56
  }
57
+ _iap = typeof mod?.initConnection === "function" ? mod : mod?.default ?? mod;
58
+ if (typeof _iap?.initConnection !== "function") {
59
+ throw new Error("@entitlehub/react-native: the 'expo-iap' module did not load correctly (no initConnection). Check that expo-iap is installed and its version supports the OpenIAP API.");
60
+ }
61
+ return _iap;
62
+ }
63
+ function iap() {
64
+ if (!_iap) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub purchases.");
44
65
  return _iap;
45
66
  }
46
- var RN_SDK_VERSION = "0.1.7";
67
+ var RN_SDK_VERSION = "0.1.8";
47
68
  var client;
48
69
  function eh() {
49
70
  if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
@@ -51,6 +72,7 @@ function eh() {
51
72
  }
52
73
  async function configureEntitleHub(opts) {
53
74
  if (opts.iap) _iap = opts.iap;
75
+ else await loadIap();
54
76
  client = new import_sdk.EntitleHub({
55
77
  apiKey: opts.apiKey,
56
78
  appUserId: opts.appUserId,
@@ -179,7 +201,7 @@ async function purchaseProduct(productId, opts = {}) {
179
201
  }
180
202
  fail(new PurchaseError(
181
203
  "purchase-pending",
182
- "The store hasn't confirmed this purchase yet. It may still complete \u2014 show a pending state, not a failure.",
204
+ "The store hasn't confirmed this purchase yet. It may still complete, show a pending state, not a failure.",
183
205
  lastInfo
184
206
  ));
185
207
  };
@@ -212,12 +234,16 @@ async function purchaseProduct(productId, opts = {}) {
212
234
  armTimer(sheetTimeoutMs);
213
235
  });
214
236
  }
215
- async function restorePurchases() {
237
+ async function restorePurchases(options = {}) {
216
238
  const I = iap();
217
239
  const purchases = await I.getAvailablePurchases() || [];
218
240
  for (const p of purchases) {
219
- await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true).catch(() => {
220
- });
241
+ try {
242
+ await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true);
243
+ } catch (err) {
244
+ if (options.onReportError) options.onReportError(err, p);
245
+ else console.warn("[entitlehub] restore: could not report", p?.productId ?? p?.id ?? p?.sku, err);
246
+ }
221
247
  }
222
248
  return eh().getCustomerInfo({ fetchPolicy: "network-only" });
223
249
  }
@@ -247,6 +273,7 @@ async function reportToEntitleHub(purchase, productId, isSubscription) {
247
273
  getOfferings,
248
274
  getProducts,
249
275
  isEntitled,
276
+ isReceiptRevoked,
250
277
  logIn,
251
278
  purchaseProduct,
252
279
  restorePurchases
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CustomerInfo, Offerings } from '@entitlehub/sdk';
2
- export { CustomerInfo, Offerings } from '@entitlehub/sdk';
2
+ export { CustomerInfo, Offerings, isReceiptRevoked } from '@entitlehub/sdk';
3
3
 
4
4
  interface ConfigureOptions {
5
5
  /** Your EntitleHub publishable key (pk_live_… / pk_test_…). Client-safe. */
@@ -13,9 +13,8 @@ interface ConfigureOptions {
13
13
  * `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
14
14
  * `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
15
15
  *
16
- * Pass your own if you need a specific version or a different OpenIAP-compatible library, e.g.
17
- * `iap: require("expo-iap")` with a pinned version, or `react-native-iap`. Useful to work around
18
- * a native issue in a particular store-library version without changing this SDK.
16
+ * Optional. Pass your own only to pin a specific version or to use a different OpenIAP-compatible
17
+ * library (e.g. `react-native-iap`); expo-iap is loaded for you otherwise.
19
18
  */
20
19
  iap?: unknown;
21
20
  }
@@ -31,13 +30,13 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
31
30
  declare function getOfferings(): Promise<Offerings>;
32
31
  /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
33
32
  declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
34
- /** Why a purchase didn't complete. Check `code` never match on the message. */
33
+ /** Why a purchase didn't complete. Check `code`, never match on the message. */
35
34
  type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
36
35
  /**
37
36
  * A purchase that didn't return entitlements.
38
37
  *
39
38
  * `code === "purchase-pending"` is NOT a failure: the store took the money but confirmation hasn't
40
- * reached us yet (store notifications are asynchronous). Show "still confirming", not an error
39
+ * reached us yet (store notifications are asynchronous). Show "still confirming", not an error,
41
40
  * the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
42
41
  */
43
42
  declare class PurchaseError extends Error {
@@ -54,18 +53,18 @@ declare class PurchaseError extends Error {
54
53
  *
55
54
  * Store bridges disagree about how the result comes back, so we accept it from any source and never
56
55
  * hang waiting for one of them:
57
- * 1. `purchaseUpdatedListener` expo-iap's event-based flow.
58
- * 2. `requestPurchase`'s resolved value react-native-iap 15+ / Nitro resolves with the purchase
56
+ * 1. `purchaseUpdatedListener`: expo-iap's event-based flow.
57
+ * 2. `requestPurchase`'s resolved value: react-native-iap 15+ / Nitro resolves with the purchase
59
58
  * and may never emit the event.
60
- * 3. Reconciliation after `timeoutMs` re-read `getAvailablePurchases()`, and failing that check
59
+ * 3. Reconciliation after `timeoutMs`: re-read `getAvailablePurchases()`, and failing that check
61
60
  * whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
62
61
  * grants it independently of the client).
63
62
  *
64
63
  * Rejects with a `PurchaseError`; branch on `.code`, never the message:
65
- * - `purchase-cancelled` the user backed out.
66
- * - `purchase-pending` the store took the money but hasn't confirmed. NOT a failure; show a
64
+ * - `purchase-cancelled`: the user backed out.
65
+ * - `purchase-pending` : the store took the money but hasn't confirmed. NOT a failure; show a
67
66
  * pending state. `.pending` is true and the entitlement will land on its own.
68
- * - `purchase-failed` a real failure.
67
+ * - `purchase-failed` : a real failure.
69
68
  */
70
69
  declare function purchaseProduct(productId: string, opts?: {
71
70
  isSubscription?: boolean;
@@ -77,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
77
76
  /** How long to poll for confirmation before reporting pending (default 30s). */
78
77
  reconcileMs?: number;
79
78
  }): Promise<CustomerInfo>;
80
- /** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
81
- declare function restorePurchases(): Promise<CustomerInfo>;
79
+
80
+ interface RestoreOptions {
81
+ /**
82
+ * Called for each purchase that could not be reported. The restore continues either way, so one
83
+ * stale receipt can't cost the user the rest of their purchases.
84
+ *
85
+ * Worth handling: a restore is where a REFUND surfaces. Apple stamps a refunded transaction with
86
+ * revocationDate permanently and StoreKit keeps handing it back, so reporting it answers
87
+ * 409 `receipt_revoked` (EntitleHub revokes the grant server-side at the same time). That 409 is
88
+ * the only signal telling you the entitlement went away because the money was returned, rather
89
+ * than for some unexplained reason.
90
+ */
91
+ onReportError?: (error: unknown, purchase: unknown) => void;
92
+ }
93
+ declare function restorePurchases(options?: RestoreOptions): Promise<CustomerInfo>;
82
94
  /** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
83
95
  declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
84
96
 
85
- export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
97
+ export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, type RestoreOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CustomerInfo, Offerings } from '@entitlehub/sdk';
2
- export { CustomerInfo, Offerings } from '@entitlehub/sdk';
2
+ export { CustomerInfo, Offerings, isReceiptRevoked } from '@entitlehub/sdk';
3
3
 
4
4
  interface ConfigureOptions {
5
5
  /** Your EntitleHub publishable key (pk_live_… / pk_test_…). Client-safe. */
@@ -13,9 +13,8 @@ interface ConfigureOptions {
13
13
  * `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
14
14
  * `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
15
15
  *
16
- * Pass your own if you need a specific version or a different OpenIAP-compatible library, e.g.
17
- * `iap: require("expo-iap")` with a pinned version, or `react-native-iap`. Useful to work around
18
- * a native issue in a particular store-library version without changing this SDK.
16
+ * Optional. Pass your own only to pin a specific version or to use a different OpenIAP-compatible
17
+ * library (e.g. `react-native-iap`); expo-iap is loaded for you otherwise.
19
18
  */
20
19
  iap?: unknown;
21
20
  }
@@ -31,13 +30,13 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
31
30
  declare function getOfferings(): Promise<Offerings>;
32
31
  /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
33
32
  declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
34
- /** Why a purchase didn't complete. Check `code` never match on the message. */
33
+ /** Why a purchase didn't complete. Check `code`, never match on the message. */
35
34
  type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
36
35
  /**
37
36
  * A purchase that didn't return entitlements.
38
37
  *
39
38
  * `code === "purchase-pending"` is NOT a failure: the store took the money but confirmation hasn't
40
- * reached us yet (store notifications are asynchronous). Show "still confirming", not an error
39
+ * reached us yet (store notifications are asynchronous). Show "still confirming", not an error,
41
40
  * the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
42
41
  */
43
42
  declare class PurchaseError extends Error {
@@ -54,18 +53,18 @@ declare class PurchaseError extends Error {
54
53
  *
55
54
  * Store bridges disagree about how the result comes back, so we accept it from any source and never
56
55
  * hang waiting for one of them:
57
- * 1. `purchaseUpdatedListener` expo-iap's event-based flow.
58
- * 2. `requestPurchase`'s resolved value react-native-iap 15+ / Nitro resolves with the purchase
56
+ * 1. `purchaseUpdatedListener`: expo-iap's event-based flow.
57
+ * 2. `requestPurchase`'s resolved value: react-native-iap 15+ / Nitro resolves with the purchase
59
58
  * and may never emit the event.
60
- * 3. Reconciliation after `timeoutMs` re-read `getAvailablePurchases()`, and failing that check
59
+ * 3. Reconciliation after `timeoutMs`: re-read `getAvailablePurchases()`, and failing that check
61
60
  * whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
62
61
  * grants it independently of the client).
63
62
  *
64
63
  * Rejects with a `PurchaseError`; branch on `.code`, never the message:
65
- * - `purchase-cancelled` the user backed out.
66
- * - `purchase-pending` the store took the money but hasn't confirmed. NOT a failure; show a
64
+ * - `purchase-cancelled`: the user backed out.
65
+ * - `purchase-pending` : the store took the money but hasn't confirmed. NOT a failure; show a
67
66
  * pending state. `.pending` is true and the entitlement will land on its own.
68
- * - `purchase-failed` a real failure.
67
+ * - `purchase-failed` : a real failure.
69
68
  */
70
69
  declare function purchaseProduct(productId: string, opts?: {
71
70
  isSubscription?: boolean;
@@ -77,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
77
76
  /** How long to poll for confirmation before reporting pending (default 30s). */
78
77
  reconcileMs?: number;
79
78
  }): Promise<CustomerInfo>;
80
- /** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
81
- declare function restorePurchases(): Promise<CustomerInfo>;
79
+
80
+ interface RestoreOptions {
81
+ /**
82
+ * Called for each purchase that could not be reported. The restore continues either way, so one
83
+ * stale receipt can't cost the user the rest of their purchases.
84
+ *
85
+ * Worth handling: a restore is where a REFUND surfaces. Apple stamps a refunded transaction with
86
+ * revocationDate permanently and StoreKit keeps handing it back, so reporting it answers
87
+ * 409 `receipt_revoked` (EntitleHub revokes the grant server-side at the same time). That 409 is
88
+ * the only signal telling you the entitlement went away because the money was returned, rather
89
+ * than for some unexplained reason.
90
+ */
91
+ onReportError?: (error: unknown, purchase: unknown) => void;
92
+ }
93
+ declare function restorePurchases(options?: RestoreOptions): Promise<CustomerInfo>;
82
94
  /** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
83
95
  declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
84
96
 
85
- export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
97
+ export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, type RestoreOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
package/dist/index.js CHANGED
@@ -1,23 +1,26 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/index.ts
9
2
  import { EntitleHub } from "@entitlehub/sdk";
3
+ import { isReceiptRevoked } from "@entitlehub/sdk";
10
4
  var _iap;
11
- function iap() {
5
+ async function loadIap() {
12
6
  if (_iap) return _iap;
7
+ let mod;
13
8
  try {
14
- _iap = __require("expo-iap");
9
+ mod = await import("expo-iap");
15
10
  } catch {
16
11
  throw new Error("@entitlehub/react-native needs 'expo-iap'. Install it with: npx expo install expo-iap");
17
12
  }
13
+ _iap = typeof mod?.initConnection === "function" ? mod : mod?.default ?? mod;
14
+ if (typeof _iap?.initConnection !== "function") {
15
+ throw new Error("@entitlehub/react-native: the 'expo-iap' module did not load correctly (no initConnection). Check that expo-iap is installed and its version supports the OpenIAP API.");
16
+ }
17
+ return _iap;
18
+ }
19
+ function iap() {
20
+ if (!_iap) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub purchases.");
18
21
  return _iap;
19
22
  }
20
- var RN_SDK_VERSION = "0.1.7";
23
+ var RN_SDK_VERSION = "0.1.8";
21
24
  var client;
22
25
  function eh() {
23
26
  if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
@@ -25,6 +28,7 @@ function eh() {
25
28
  }
26
29
  async function configureEntitleHub(opts) {
27
30
  if (opts.iap) _iap = opts.iap;
31
+ else await loadIap();
28
32
  client = new EntitleHub({
29
33
  apiKey: opts.apiKey,
30
34
  appUserId: opts.appUserId,
@@ -153,7 +157,7 @@ async function purchaseProduct(productId, opts = {}) {
153
157
  }
154
158
  fail(new PurchaseError(
155
159
  "purchase-pending",
156
- "The store hasn't confirmed this purchase yet. It may still complete \u2014 show a pending state, not a failure.",
160
+ "The store hasn't confirmed this purchase yet. It may still complete, show a pending state, not a failure.",
157
161
  lastInfo
158
162
  ));
159
163
  };
@@ -186,12 +190,16 @@ async function purchaseProduct(productId, opts = {}) {
186
190
  armTimer(sheetTimeoutMs);
187
191
  });
188
192
  }
189
- async function restorePurchases() {
193
+ async function restorePurchases(options = {}) {
190
194
  const I = iap();
191
195
  const purchases = await I.getAvailablePurchases() || [];
192
196
  for (const p of purchases) {
193
- await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true).catch(() => {
194
- });
197
+ try {
198
+ await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true);
199
+ } catch (err) {
200
+ if (options.onReportError) options.onReportError(err, p);
201
+ else console.warn("[entitlehub] restore: could not report", p?.productId ?? p?.id ?? p?.sku, err);
202
+ }
195
203
  }
196
204
  return eh().getCustomerInfo({ fetchPolicy: "network-only" });
197
205
  }
@@ -220,6 +228,7 @@ export {
220
228
  getOfferings,
221
229
  getProducts,
222
230
  isEntitled,
231
+ isReceiptRevoked,
223
232
  logIn,
224
233
  purchaseProduct,
225
234
  restorePurchases
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@entitlehub/react-native",
3
- "version": "0.1.7",
4
- "description": "EntitleHub for React Native & Expo one call to purchase and unlock entitlements. Wraps expo-iap for the native store sheet and validates receipts via EntitleHub.",
3
+ "version": "0.1.9",
4
+ "description": "EntitleHub for React Native & Expo, one call to purchase and unlock entitlements. Wraps expo-iap for the native store sheet and validates receipts via EntitleHub.",
5
5
  "keywords": [
6
6
  "entitlehub",
7
7
  "react-native",
@@ -47,13 +47,14 @@
47
47
  "scripts": {
48
48
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
49
49
  "typecheck": "tsc --noEmit",
50
+ "test": "npm run build && node --test \"test/*.test.mjs\"",
50
51
  "prepublishOnly": "npm run build"
51
52
  },
52
53
  "engines": {
53
54
  "node": ">=18"
54
55
  },
55
56
  "peerDependencies": {
56
- "@entitlehub/sdk": ">=0.2.0",
57
+ "@entitlehub/sdk": ">=0.3.1",
57
58
  "expo-iap": "*",
58
59
  "react": ">=17"
59
60
  },