@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 +22 -0
- package/README.md +22 -0
- package/dist/index.cjs +60 -11
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +60 -11
- package/package.json +1 -1
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().
|
|
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
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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?.
|
|
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)
|
|
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
|
-
*
|
|
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)
|
|
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
|
-
*
|
|
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().
|
|
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
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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?.
|
|
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.
|
|
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",
|