@entitlehub/react-native 0.1.0 → 0.1.2

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 — 2026-07-18
4
+
5
+ ### Added
6
+ - `configureEntitleHub({ iap })` — inject the OpenIAP-compatible store module (pinned expo-iap
7
+ version, or react-native-iap) instead of the hard-coded `expo-iap`. Decouples this SDK from a
8
+ single store-library version so you can work around a native issue without changing the SDK.
9
+
10
+ ### Docs
11
+ - Note that native store-library crashes (e.g. `EXC_BAD_ACCESS` in expo-iap/OpenIAP off the JS
12
+ thread) are a store-library-version concern; how to pin a version, inject a module, or fall back
13
+ to the fully-decoupled `@entitlehub/sdk` + your own billing library.
14
+
15
+ ## 0.1.1 — 2026-07-18
16
+
17
+ ### Fixed
18
+ - **`purchaseProduct` crashed** — 0.1.0 was written against expo-iap's old flat `requestPurchase`
19
+ API and read the purchase from the return value. `expo-iap@4.x` is **event-based** with a
20
+ discriminated request. Now: `requestPurchase({ request: { apple: { sku }, google: { skus } },
21
+ type })` with the result bridged from `purchaseUpdatedListener` / `purchaseErrorListener`.
22
+ - Android subscriptions now pass the required `offerToken` (fetched from the product).
23
+ - `getProducts` uses expo-iap's `fetchProducts` (the old `getProducts` no longer exists).
24
+
3
25
  ## 0.1.0 — 2026-07-18
4
26
 
5
27
  Initial release.
package/README.md CHANGED
@@ -57,6 +57,28 @@ customer info. Renewals/refunds stay current via
57
57
 
58
58
  Full guide: **[entitlehub.com/docs/purchases](https://entitlehub.com/docs/purchases)**.
59
59
 
60
+ ## Choosing / pinning the store library
61
+
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
64
+ bugs. If you hit a **native IAP crash** (e.g. an `EXC_BAD_ACCESS` in the store module), it's the
65
+ store library, not this SDK or your app code, and no JS change can catch a native exception thrown
66
+ off the JS thread.
67
+
68
+ Two levers, both without changing this SDK:
69
+
70
+ - **Pin a working version:** `npm install expo-iap@<version>` — this package uses whatever `expo-iap`
71
+ is installed (peer dependency).
72
+ - **Supply your own module:** pass any OpenIAP-compatible library to `configureEntitleHub`:
73
+ ```ts
74
+ await configureEntitleHub({ apiKey, appUserId, iap: require("expo-iap") }); // or react-native-iap
75
+ ```
76
+
77
+ 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
79
+ `eh.reportPurchase({ signedTransaction | purchaseToken })`. See
80
+ [Purchases → manual](https://entitlehub.com/docs/purchases).
81
+
60
82
  > Requires a development build (Expo Go has no in-app-purchase native module). The entitlement layer
61
83
  > is [`@entitlehub/sdk`](https://www.npmjs.com/package/@entitlehub/sdk); this package adds the
62
84
  > purchase flow on top.
package/dist/index.cjs CHANGED
@@ -48,6 +48,7 @@ function eh() {
48
48
  return client;
49
49
  }
50
50
  async function configureEntitleHub(opts) {
51
+ if (opts.iap) _iap = opts.iap;
51
52
  client = new import_sdk.EntitleHub({ apiKey: opts.apiKey, appUserId: opts.appUserId, baseUrl: opts.baseUrl });
52
53
  try {
53
54
  await iap().initConnection();
@@ -66,20 +67,68 @@ async function isEntitled(entitlementId) {
66
67
  async function getOfferings() {
67
68
  return eh().getOfferings();
68
69
  }
69
- async function getProducts(productIds) {
70
- return iap().getProducts(productIds);
70
+ async function getProducts(productIds, type = "all") {
71
+ return iap().fetchProducts({ skus: productIds, type });
71
72
  }
72
73
  async function purchaseProduct(productId, opts = {}) {
73
74
  const I = iap();
74
- const result = await I.requestPurchase({ sku: productId, skus: [productId] });
75
- const purchase = Array.isArray(result) ? result[0] : result;
76
- if (!purchase) throw new Error("purchase-cancelled");
77
- const info = await reportToEntitleHub(purchase, productId, opts.isSubscription);
78
- try {
79
- await I.finishTransaction({ purchase, isConsumable: false });
80
- } catch {
75
+ const isSub = Boolean(opts.isSubscription);
76
+ const request = { apple: { sku: productId }, google: { skus: [productId] } };
77
+ if (isSub) {
78
+ try {
79
+ const products = await I.fetchProducts({ skus: [productId], type: "subs" });
80
+ const product = Array.isArray(products) ? products.find((p) => (p?.id ?? p?.productId) === productId) : void 0;
81
+ const offer = product?.subscriptionOfferDetailsAndroid?.[0] ?? product?.subscriptionOfferDetails?.[0];
82
+ if (offer?.offerToken) {
83
+ request.google = { skus: [productId], subscriptionOffers: [{ sku: productId, offerToken: offer.offerToken }] };
84
+ }
85
+ } catch {
86
+ }
81
87
  }
82
- return info;
88
+ return new Promise((resolve, reject) => {
89
+ let settled = false;
90
+ const cleanup = () => {
91
+ try {
92
+ updSub?.remove();
93
+ } catch {
94
+ }
95
+ try {
96
+ errSub?.remove();
97
+ } catch {
98
+ }
99
+ };
100
+ const updSub = I.purchaseUpdatedListener(async (purchase) => {
101
+ if (settled) return;
102
+ settled = true;
103
+ cleanup();
104
+ try {
105
+ const info = await reportToEntitleHub(purchase, productId, isSub);
106
+ try {
107
+ await I.finishTransaction({ purchase, isConsumable: false });
108
+ } catch {
109
+ }
110
+ resolve(info);
111
+ } catch (e) {
112
+ reject(e);
113
+ }
114
+ });
115
+ const errSub = I.purchaseErrorListener((error) => {
116
+ if (settled) return;
117
+ settled = true;
118
+ cleanup();
119
+ const code = error?.code;
120
+ const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
121
+ reject(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
122
+ });
123
+ Promise.resolve(
124
+ I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
125
+ ).catch((e) => {
126
+ if (settled) return;
127
+ settled = true;
128
+ cleanup();
129
+ reject(e);
130
+ });
131
+ });
83
132
  }
84
133
  async function restorePurchases() {
85
134
  const I = iap();
@@ -94,7 +143,7 @@ function addCustomerInfoUpdateListener(fn) {
94
143
  return eh().addCustomerInfoUpdateListener(fn);
95
144
  }
96
145
  async function reportToEntitleHub(purchase, productId, isSubscription) {
97
- const jws = purchase?.jwsRepresentationIos ?? purchase?.jwsRepresentation ?? purchase?.verificationResultIos;
146
+ const jws = purchase?.jwsRepresentation ?? purchase?.jwsRepresentationIos;
98
147
  if (jws) return eh().reportPurchase({ signedTransaction: jws });
99
148
  const token = purchase?.purchaseTokenAndroid ?? purchase?.purchaseToken;
100
149
  if (token) {
package/dist/index.d.cts CHANGED
@@ -8,6 +8,16 @@ interface ConfigureOptions {
8
8
  appUserId: string;
9
9
  /** Override the API base (self-host). */
10
10
  baseUrl?: string;
11
+ /**
12
+ * The in-app-purchase module to use. Must implement the OpenIAP API (`initConnection`,
13
+ * `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
14
+ * `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
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.
19
+ */
20
+ iap?: unknown;
11
21
  }
12
22
  /** Configure EntitleHub and open the store connection. Call once at startup (after your own login). */
13
23
  declare function configureEntitleHub(opts: ConfigureOptions): Promise<void>;
@@ -19,12 +29,15 @@ declare function getCustomerInfo(): Promise<CustomerInfo>;
19
29
  declare function isEntitled(entitlementId: string): Promise<boolean>;
20
30
  /** Your EntitleHub catalog (entitlements + products) for building a paywall. */
21
31
  declare function getOfferings(): Promise<Offerings>;
22
- /** Live store products (prices, localized titles) for the given ids, from the native store. */
23
- declare function getProducts(productIds: string[]): Promise<any[]>;
32
+ /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
33
+ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
24
34
  /**
25
35
  * Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
26
36
  * and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
27
- * Throws "purchase-cancelled" if the user backs out.
37
+ *
38
+ * expo-iap's purchase flow is event-based — the result arrives on `purchaseUpdatedListener`, not the
39
+ * `requestPurchase` return value — so we bridge that back into a promise here.
40
+ * Rejects with "purchase-cancelled" if the user backs out.
28
41
  */
29
42
  declare function purchaseProduct(productId: string, opts?: {
30
43
  isSubscription?: boolean;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,16 @@ interface ConfigureOptions {
8
8
  appUserId: string;
9
9
  /** Override the API base (self-host). */
10
10
  baseUrl?: string;
11
+ /**
12
+ * The in-app-purchase module to use. Must implement the OpenIAP API (`initConnection`,
13
+ * `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
14
+ * `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
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.
19
+ */
20
+ iap?: unknown;
11
21
  }
12
22
  /** Configure EntitleHub and open the store connection. Call once at startup (after your own login). */
13
23
  declare function configureEntitleHub(opts: ConfigureOptions): Promise<void>;
@@ -19,12 +29,15 @@ declare function getCustomerInfo(): Promise<CustomerInfo>;
19
29
  declare function isEntitled(entitlementId: string): Promise<boolean>;
20
30
  /** Your EntitleHub catalog (entitlements + products) for building a paywall. */
21
31
  declare function getOfferings(): Promise<Offerings>;
22
- /** Live store products (prices, localized titles) for the given ids, from the native store. */
23
- declare function getProducts(productIds: string[]): Promise<any[]>;
32
+ /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
33
+ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
24
34
  /**
25
35
  * Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
26
36
  * and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
27
- * Throws "purchase-cancelled" if the user backs out.
37
+ *
38
+ * expo-iap's purchase flow is event-based — the result arrives on `purchaseUpdatedListener`, not the
39
+ * `requestPurchase` return value — so we bridge that back into a promise here.
40
+ * Rejects with "purchase-cancelled" if the user backs out.
28
41
  */
29
42
  declare function purchaseProduct(productId: string, opts?: {
30
43
  isSubscription?: boolean;
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ function eh() {
23
23
  return client;
24
24
  }
25
25
  async function configureEntitleHub(opts) {
26
+ if (opts.iap) _iap = opts.iap;
26
27
  client = new EntitleHub({ apiKey: opts.apiKey, appUserId: opts.appUserId, baseUrl: opts.baseUrl });
27
28
  try {
28
29
  await iap().initConnection();
@@ -41,20 +42,68 @@ async function isEntitled(entitlementId) {
41
42
  async function getOfferings() {
42
43
  return eh().getOfferings();
43
44
  }
44
- async function getProducts(productIds) {
45
- return iap().getProducts(productIds);
45
+ async function getProducts(productIds, type = "all") {
46
+ return iap().fetchProducts({ skus: productIds, type });
46
47
  }
47
48
  async function purchaseProduct(productId, opts = {}) {
48
49
  const I = iap();
49
- const result = await I.requestPurchase({ sku: productId, skus: [productId] });
50
- const purchase = Array.isArray(result) ? result[0] : result;
51
- if (!purchase) throw new Error("purchase-cancelled");
52
- const info = await reportToEntitleHub(purchase, productId, opts.isSubscription);
53
- try {
54
- await I.finishTransaction({ purchase, isConsumable: false });
55
- } catch {
50
+ const isSub = Boolean(opts.isSubscription);
51
+ const request = { apple: { sku: productId }, google: { skus: [productId] } };
52
+ if (isSub) {
53
+ try {
54
+ const products = await I.fetchProducts({ skus: [productId], type: "subs" });
55
+ const product = Array.isArray(products) ? products.find((p) => (p?.id ?? p?.productId) === productId) : void 0;
56
+ const offer = product?.subscriptionOfferDetailsAndroid?.[0] ?? product?.subscriptionOfferDetails?.[0];
57
+ if (offer?.offerToken) {
58
+ request.google = { skus: [productId], subscriptionOffers: [{ sku: productId, offerToken: offer.offerToken }] };
59
+ }
60
+ } catch {
61
+ }
56
62
  }
57
- return info;
63
+ return new Promise((resolve, reject) => {
64
+ let settled = false;
65
+ const cleanup = () => {
66
+ try {
67
+ updSub?.remove();
68
+ } catch {
69
+ }
70
+ try {
71
+ errSub?.remove();
72
+ } catch {
73
+ }
74
+ };
75
+ const updSub = I.purchaseUpdatedListener(async (purchase) => {
76
+ if (settled) return;
77
+ settled = true;
78
+ cleanup();
79
+ try {
80
+ const info = await reportToEntitleHub(purchase, productId, isSub);
81
+ try {
82
+ await I.finishTransaction({ purchase, isConsumable: false });
83
+ } catch {
84
+ }
85
+ resolve(info);
86
+ } catch (e) {
87
+ reject(e);
88
+ }
89
+ });
90
+ const errSub = I.purchaseErrorListener((error) => {
91
+ if (settled) return;
92
+ settled = true;
93
+ cleanup();
94
+ const code = error?.code;
95
+ const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
96
+ reject(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
97
+ });
98
+ Promise.resolve(
99
+ I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
100
+ ).catch((e) => {
101
+ if (settled) return;
102
+ settled = true;
103
+ cleanup();
104
+ reject(e);
105
+ });
106
+ });
58
107
  }
59
108
  async function restorePurchases() {
60
109
  const I = iap();
@@ -69,7 +118,7 @@ function addCustomerInfoUpdateListener(fn) {
69
118
  return eh().addCustomerInfoUpdateListener(fn);
70
119
  }
71
120
  async function reportToEntitleHub(purchase, productId, isSubscription) {
72
- const jws = purchase?.jwsRepresentationIos ?? purchase?.jwsRepresentation ?? purchase?.verificationResultIos;
121
+ const jws = purchase?.jwsRepresentation ?? purchase?.jwsRepresentationIos;
73
122
  if (jws) return eh().reportPurchase({ signedTransaction: jws });
74
123
  const token = purchase?.purchaseTokenAndroid ?? purchase?.purchaseToken;
75
124
  if (token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entitlehub/react-native",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",