@entitlehub/react-native 0.1.8 → 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,5 +1,35 @@
1
1
  # Changelog
2
2
 
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
+
3
33
  ## 0.1.8 (2026-09-19)
4
34
 
5
35
  ### Fixed
package/dist/index.cjs CHANGED
@@ -37,12 +37,14 @@ __export(index_exports, {
37
37
  getOfferings: () => getOfferings,
38
38
  getProducts: () => getProducts,
39
39
  isEntitled: () => isEntitled,
40
+ isReceiptRevoked: () => import_sdk2.isReceiptRevoked,
40
41
  logIn: () => logIn,
41
42
  purchaseProduct: () => purchaseProduct,
42
43
  restorePurchases: () => restorePurchases
43
44
  });
44
45
  module.exports = __toCommonJS(index_exports);
45
46
  var import_sdk = require("@entitlehub/sdk");
47
+ var import_sdk2 = require("@entitlehub/sdk");
46
48
  var _iap;
47
49
  async function loadIap() {
48
50
  if (_iap) return _iap;
@@ -232,12 +234,16 @@ async function purchaseProduct(productId, opts = {}) {
232
234
  armTimer(sheetTimeoutMs);
233
235
  });
234
236
  }
235
- async function restorePurchases() {
237
+ async function restorePurchases(options = {}) {
236
238
  const I = iap();
237
239
  const purchases = await I.getAvailablePurchases() || [];
238
240
  for (const p of purchases) {
239
- await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true).catch(() => {
240
- });
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
+ }
241
247
  }
242
248
  return eh().getCustomerInfo({ fetchPolicy: "network-only" });
243
249
  }
@@ -267,6 +273,7 @@ async function reportToEntitleHub(purchase, productId, isSubscription) {
267
273
  getOfferings,
268
274
  getProducts,
269
275
  isEntitled,
276
+ isReceiptRevoked,
270
277
  logIn,
271
278
  purchaseProduct,
272
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. */
@@ -76,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
76
76
  /** How long to poll for confirmation before reporting pending (default 30s). */
77
77
  reconcileMs?: number;
78
78
  }): Promise<CustomerInfo>;
79
- /** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
80
- 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>;
81
94
  /** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
82
95
  declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
83
96
 
84
- 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. */
@@ -76,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
76
76
  /** How long to poll for confirmation before reporting pending (default 30s). */
77
77
  reconcileMs?: number;
78
78
  }): Promise<CustomerInfo>;
79
- /** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
80
- 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>;
81
94
  /** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
82
95
  declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
83
96
 
84
- 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,5 +1,6 @@
1
1
  // src/index.ts
2
2
  import { EntitleHub } from "@entitlehub/sdk";
3
+ import { isReceiptRevoked } from "@entitlehub/sdk";
3
4
  var _iap;
4
5
  async function loadIap() {
5
6
  if (_iap) return _iap;
@@ -189,12 +190,16 @@ async function purchaseProduct(productId, opts = {}) {
189
190
  armTimer(sheetTimeoutMs);
190
191
  });
191
192
  }
192
- async function restorePurchases() {
193
+ async function restorePurchases(options = {}) {
193
194
  const I = iap();
194
195
  const purchases = await I.getAvailablePurchases() || [];
195
196
  for (const p of purchases) {
196
- await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true).catch(() => {
197
- });
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
+ }
198
203
  }
199
204
  return eh().getCustomerInfo({ fetchPolicy: "network-only" });
200
205
  }
@@ -223,6 +228,7 @@ export {
223
228
  getOfferings,
224
229
  getProducts,
225
230
  isEntitled,
231
+ isReceiptRevoked,
226
232
  logIn,
227
233
  purchaseProduct,
228
234
  restorePurchases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entitlehub/react-native",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
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",
@@ -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
  },