@entitlehub/react-native 0.1.0 → 0.1.1

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 — 2026-07-18
4
+
5
+ ### Fixed
6
+ - **`purchaseProduct` crashed** — 0.1.0 was written against expo-iap's old flat `requestPurchase`
7
+ API and read the purchase from the return value. `expo-iap@4.x` is **event-based** with a
8
+ discriminated request. Now: `requestPurchase({ request: { apple: { sku }, google: { skus } },
9
+ type })` with the result bridged from `purchaseUpdatedListener` / `purchaseErrorListener`.
10
+ - Android subscriptions now pass the required `offerToken` (fetched from the product).
11
+ - `getProducts` uses expo-iap's `fetchProducts` (the old `getProducts` no longer exists).
12
+
3
13
  ## 0.1.0 — 2026-07-18
4
14
 
5
15
  Initial release.
package/dist/index.cjs CHANGED
@@ -66,20 +66,68 @@ async function isEntitled(entitlementId) {
66
66
  async function getOfferings() {
67
67
  return eh().getOfferings();
68
68
  }
69
- async function getProducts(productIds) {
70
- return iap().getProducts(productIds);
69
+ async function getProducts(productIds, type = "all") {
70
+ return iap().fetchProducts({ skus: productIds, type });
71
71
  }
72
72
  async function purchaseProduct(productId, opts = {}) {
73
73
  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 {
74
+ const isSub = Boolean(opts.isSubscription);
75
+ const request = { apple: { sku: productId }, google: { skus: [productId] } };
76
+ if (isSub) {
77
+ try {
78
+ const products = await I.fetchProducts({ skus: [productId], type: "subs" });
79
+ const product = Array.isArray(products) ? products.find((p) => (p?.id ?? p?.productId) === productId) : void 0;
80
+ const offer = product?.subscriptionOfferDetailsAndroid?.[0] ?? product?.subscriptionOfferDetails?.[0];
81
+ if (offer?.offerToken) {
82
+ request.google = { skus: [productId], subscriptionOffers: [{ sku: productId, offerToken: offer.offerToken }] };
83
+ }
84
+ } catch {
85
+ }
81
86
  }
82
- return info;
87
+ return new Promise((resolve, reject) => {
88
+ let settled = false;
89
+ const cleanup = () => {
90
+ try {
91
+ updSub?.remove();
92
+ } catch {
93
+ }
94
+ try {
95
+ errSub?.remove();
96
+ } catch {
97
+ }
98
+ };
99
+ const updSub = I.purchaseUpdatedListener(async (purchase) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ cleanup();
103
+ try {
104
+ const info = await reportToEntitleHub(purchase, productId, isSub);
105
+ try {
106
+ await I.finishTransaction({ purchase, isConsumable: false });
107
+ } catch {
108
+ }
109
+ resolve(info);
110
+ } catch (e) {
111
+ reject(e);
112
+ }
113
+ });
114
+ const errSub = I.purchaseErrorListener((error) => {
115
+ if (settled) return;
116
+ settled = true;
117
+ cleanup();
118
+ const code = error?.code;
119
+ const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
120
+ reject(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
121
+ });
122
+ Promise.resolve(
123
+ I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
124
+ ).catch((e) => {
125
+ if (settled) return;
126
+ settled = true;
127
+ cleanup();
128
+ reject(e);
129
+ });
130
+ });
83
131
  }
84
132
  async function restorePurchases() {
85
133
  const I = iap();
@@ -94,7 +142,7 @@ function addCustomerInfoUpdateListener(fn) {
94
142
  return eh().addCustomerInfoUpdateListener(fn);
95
143
  }
96
144
  async function reportToEntitleHub(purchase, productId, isSubscription) {
97
- const jws = purchase?.jwsRepresentationIos ?? purchase?.jwsRepresentation ?? purchase?.verificationResultIos;
145
+ const jws = purchase?.jwsRepresentation ?? purchase?.jwsRepresentationIos;
98
146
  if (jws) return eh().reportPurchase({ signedTransaction: jws });
99
147
  const token = purchase?.purchaseTokenAndroid ?? purchase?.purchaseToken;
100
148
  if (token) {
package/dist/index.d.cts CHANGED
@@ -19,12 +19,15 @@ declare function getCustomerInfo(): Promise<CustomerInfo>;
19
19
  declare function isEntitled(entitlementId: string): Promise<boolean>;
20
20
  /** Your EntitleHub catalog (entitlements + products) for building a paywall. */
21
21
  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[]>;
22
+ /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
23
+ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
24
24
  /**
25
25
  * Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
26
26
  * and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
27
- * Throws "purchase-cancelled" if the user backs out.
27
+ *
28
+ * expo-iap's purchase flow is event-based — the result arrives on `purchaseUpdatedListener`, not the
29
+ * `requestPurchase` return value — so we bridge that back into a promise here.
30
+ * Rejects with "purchase-cancelled" if the user backs out.
28
31
  */
29
32
  declare function purchaseProduct(productId: string, opts?: {
30
33
  isSubscription?: boolean;
package/dist/index.d.ts CHANGED
@@ -19,12 +19,15 @@ declare function getCustomerInfo(): Promise<CustomerInfo>;
19
19
  declare function isEntitled(entitlementId: string): Promise<boolean>;
20
20
  /** Your EntitleHub catalog (entitlements + products) for building a paywall. */
21
21
  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[]>;
22
+ /** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
23
+ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
24
24
  /**
25
25
  * Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
26
26
  * and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
27
- * Throws "purchase-cancelled" if the user backs out.
27
+ *
28
+ * expo-iap's purchase flow is event-based — the result arrives on `purchaseUpdatedListener`, not the
29
+ * `requestPurchase` return value — so we bridge that back into a promise here.
30
+ * Rejects with "purchase-cancelled" if the user backs out.
28
31
  */
29
32
  declare function purchaseProduct(productId: string, opts?: {
30
33
  isSubscription?: boolean;
package/dist/index.js CHANGED
@@ -41,20 +41,68 @@ async function isEntitled(entitlementId) {
41
41
  async function getOfferings() {
42
42
  return eh().getOfferings();
43
43
  }
44
- async function getProducts(productIds) {
45
- return iap().getProducts(productIds);
44
+ async function getProducts(productIds, type = "all") {
45
+ return iap().fetchProducts({ skus: productIds, type });
46
46
  }
47
47
  async function purchaseProduct(productId, opts = {}) {
48
48
  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 {
49
+ const isSub = Boolean(opts.isSubscription);
50
+ const request = { apple: { sku: productId }, google: { skus: [productId] } };
51
+ if (isSub) {
52
+ try {
53
+ const products = await I.fetchProducts({ skus: [productId], type: "subs" });
54
+ const product = Array.isArray(products) ? products.find((p) => (p?.id ?? p?.productId) === productId) : void 0;
55
+ const offer = product?.subscriptionOfferDetailsAndroid?.[0] ?? product?.subscriptionOfferDetails?.[0];
56
+ if (offer?.offerToken) {
57
+ request.google = { skus: [productId], subscriptionOffers: [{ sku: productId, offerToken: offer.offerToken }] };
58
+ }
59
+ } catch {
60
+ }
56
61
  }
57
- return info;
62
+ return new Promise((resolve, reject) => {
63
+ let settled = false;
64
+ const cleanup = () => {
65
+ try {
66
+ updSub?.remove();
67
+ } catch {
68
+ }
69
+ try {
70
+ errSub?.remove();
71
+ } catch {
72
+ }
73
+ };
74
+ const updSub = I.purchaseUpdatedListener(async (purchase) => {
75
+ if (settled) return;
76
+ settled = true;
77
+ cleanup();
78
+ try {
79
+ const info = await reportToEntitleHub(purchase, productId, isSub);
80
+ try {
81
+ await I.finishTransaction({ purchase, isConsumable: false });
82
+ } catch {
83
+ }
84
+ resolve(info);
85
+ } catch (e) {
86
+ reject(e);
87
+ }
88
+ });
89
+ const errSub = I.purchaseErrorListener((error) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ cleanup();
93
+ const code = error?.code;
94
+ const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
95
+ reject(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
96
+ });
97
+ Promise.resolve(
98
+ I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
99
+ ).catch((e) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ cleanup();
103
+ reject(e);
104
+ });
105
+ });
58
106
  }
59
107
  async function restorePurchases() {
60
108
  const I = iap();
@@ -69,7 +117,7 @@ function addCustomerInfoUpdateListener(fn) {
69
117
  return eh().addCustomerInfoUpdateListener(fn);
70
118
  }
71
119
  async function reportToEntitleHub(purchase, productId, isSubscription) {
72
- const jws = purchase?.jwsRepresentationIos ?? purchase?.jwsRepresentation ?? purchase?.verificationResultIos;
120
+ const jws = purchase?.jwsRepresentation ?? purchase?.jwsRepresentationIos;
73
121
  if (jws) return eh().reportPurchase({ signedTransaction: jws });
74
122
  const token = purchase?.purchaseTokenAndroid ?? purchase?.purchaseToken;
75
123
  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.1",
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",