@entitlehub/react-native 0.1.4 → 0.1.6
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 +24 -0
- package/dist/index.cjs +57 -12
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +57 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 — 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Reports `react-native/<version>` to EntitleHub on each call, so the dashboard can tell you when
|
|
7
|
+
your SDK is behind a release with fixes in it — rather than you finding out by hitting the bug.
|
|
8
|
+
Version only; no device or user data.
|
|
9
|
+
|
|
10
|
+
## 0.1.5 — 2026-07-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`purchaseProduct()` could hang forever after a successful purchase.** It waited only on
|
|
14
|
+
`purchaseUpdatedListener`; on the react-native-iap 15+ / Nitro bridge that callback never fires,
|
|
15
|
+
so the promise never settled — the paywall stayed open and features stayed locked even though the
|
|
16
|
+
user had been charged and EntitleHub had already granted the entitlement server-side.
|
|
17
|
+
`requestPurchase`'s resolved value was being discarded entirely; it is now honoured, so bridges
|
|
18
|
+
that resolve instead of emitting an event work.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Reconciliation safety net: if no result arrives within `timeoutMs` (default 120s),
|
|
22
|
+
`purchaseProduct()` re-reads `getAvailablePurchases()` and, failing that, re-checks entitlements
|
|
23
|
+
from the server — resolving successfully if the entitlement appeared while the client was waiting.
|
|
24
|
+
Only rejects (`"purchase-timeout"`) when nothing was actually granted.
|
|
25
|
+
- `purchaseProduct(id, { timeoutMs })` to tune that window.
|
|
26
|
+
|
|
3
27
|
## 0.1.2 — 2026-07-18
|
|
4
28
|
|
|
5
29
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ function iap() {
|
|
|
42
42
|
}
|
|
43
43
|
return _iap;
|
|
44
44
|
}
|
|
45
|
+
var RN_SDK_VERSION = "0.1.6";
|
|
45
46
|
var client;
|
|
46
47
|
function eh() {
|
|
47
48
|
if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
|
|
@@ -49,7 +50,12 @@ function eh() {
|
|
|
49
50
|
}
|
|
50
51
|
async function configureEntitleHub(opts) {
|
|
51
52
|
if (opts.iap) _iap = opts.iap;
|
|
52
|
-
client = new import_sdk.EntitleHub({
|
|
53
|
+
client = new import_sdk.EntitleHub({
|
|
54
|
+
apiKey: opts.apiKey,
|
|
55
|
+
appUserId: opts.appUserId,
|
|
56
|
+
baseUrl: opts.baseUrl,
|
|
57
|
+
client: `react-native/${RN_SDK_VERSION}`
|
|
58
|
+
});
|
|
53
59
|
try {
|
|
54
60
|
await iap().initConnection();
|
|
55
61
|
} catch {
|
|
@@ -70,10 +76,17 @@ async function getOfferings() {
|
|
|
70
76
|
async function getProducts(productIds, type = "all") {
|
|
71
77
|
return iap().fetchProducts({ skus: productIds, type });
|
|
72
78
|
}
|
|
79
|
+
var DEFAULT_PURCHASE_TIMEOUT_MS = 12e4;
|
|
73
80
|
async function purchaseProduct(productId, opts = {}) {
|
|
74
81
|
const I = iap();
|
|
75
82
|
const isSub = Boolean(opts.isSubscription);
|
|
76
83
|
const isConsumable = Boolean(opts.isConsumable) && !isSub;
|
|
84
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_PURCHASE_TIMEOUT_MS;
|
|
85
|
+
let entitlementsBefore = [];
|
|
86
|
+
try {
|
|
87
|
+
entitlementsBefore = (await eh().getCustomerInfo()).activeEntitlementIds;
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
77
90
|
const request = { apple: { sku: productId }, google: { skus: [productId] } };
|
|
78
91
|
if (isSub) {
|
|
79
92
|
try {
|
|
@@ -88,6 +101,7 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
88
101
|
}
|
|
89
102
|
return new Promise((resolve, reject) => {
|
|
90
103
|
let settled = false;
|
|
104
|
+
let timer;
|
|
91
105
|
const cleanup = () => {
|
|
92
106
|
try {
|
|
93
107
|
updSub?.remove();
|
|
@@ -97,8 +111,15 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
97
111
|
errSub?.remove();
|
|
98
112
|
} catch {
|
|
99
113
|
}
|
|
114
|
+
if (timer) clearTimeout(timer);
|
|
115
|
+
};
|
|
116
|
+
const fail = (e) => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(e);
|
|
100
121
|
};
|
|
101
|
-
const
|
|
122
|
+
const settleWithPurchase = async (purchase) => {
|
|
102
123
|
if (settled) return;
|
|
103
124
|
settled = true;
|
|
104
125
|
cleanup();
|
|
@@ -112,23 +133,47 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
112
133
|
} catch (e) {
|
|
113
134
|
reject(e);
|
|
114
135
|
}
|
|
136
|
+
};
|
|
137
|
+
const isPurchase = (p) => Boolean(p) && Boolean(p.purchaseToken || p.jwsRepresentation || p.jwsRepresentationIos || p.purchaseTokenAndroid || p.transactionId || p.transactionReceipt || p.id);
|
|
138
|
+
const reconcile = async () => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
try {
|
|
141
|
+
const owned = await I.getAvailablePurchases() || [];
|
|
142
|
+
const match = owned.find((p) => (p?.productId ?? p?.id ?? p?.sku) === productId);
|
|
143
|
+
if (match) return settleWithPurchase(match);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const info = await eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
148
|
+
const grew = info.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id));
|
|
149
|
+
if (grew) {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
cleanup();
|
|
153
|
+
resolve(info);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
fail(new Error("purchase-timeout"));
|
|
159
|
+
};
|
|
160
|
+
const updSub = I.purchaseUpdatedListener((purchase) => {
|
|
161
|
+
void settleWithPurchase(purchase);
|
|
115
162
|
});
|
|
116
163
|
const errSub = I.purchaseErrorListener((error) => {
|
|
117
|
-
if (settled) return;
|
|
118
|
-
settled = true;
|
|
119
|
-
cleanup();
|
|
120
164
|
const code = error?.code;
|
|
121
165
|
const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
|
|
122
|
-
|
|
166
|
+
fail(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
|
|
123
167
|
});
|
|
124
168
|
Promise.resolve(
|
|
125
169
|
I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
|
|
126
|
-
).
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
).then((res) => {
|
|
171
|
+
const p = Array.isArray(res) ? res.find(isPurchase) : res;
|
|
172
|
+
if (isPurchase(p)) void settleWithPurchase(p);
|
|
173
|
+
}).catch((e) => fail(e));
|
|
174
|
+
timer = setTimeout(() => {
|
|
175
|
+
void reconcile();
|
|
176
|
+
}, timeoutMs);
|
|
132
177
|
});
|
|
133
178
|
}
|
|
134
179
|
async function restorePurchases() {
|
package/dist/index.d.cts
CHANGED
|
@@ -35,13 +35,22 @@ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "a
|
|
|
35
35
|
* Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
|
|
36
36
|
* and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Store bridges disagree about how the result comes back, so we accept it from any source and never
|
|
39
|
+
* hang waiting for one of them:
|
|
40
|
+
* 1. `purchaseUpdatedListener` — expo-iap's event-based flow.
|
|
41
|
+
* 2. `requestPurchase`'s resolved value — react-native-iap 15+ / Nitro resolves with the purchase
|
|
42
|
+
* and may never emit the event.
|
|
43
|
+
* 3. Reconciliation after `timeoutMs` — re-read `getAvailablePurchases()`, and failing that check
|
|
44
|
+
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
45
|
+
* grants it independently of the client).
|
|
46
|
+
*
|
|
47
|
+
* Rejects with "purchase-cancelled" if the user backs out, "purchase-timeout" if nothing arrived
|
|
48
|
+
* and nothing was granted.
|
|
41
49
|
*/
|
|
42
50
|
declare function purchaseProduct(productId: string, opts?: {
|
|
43
51
|
isSubscription?: boolean;
|
|
44
52
|
isConsumable?: boolean;
|
|
53
|
+
timeoutMs?: number;
|
|
45
54
|
}): Promise<CustomerInfo>;
|
|
46
55
|
/** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
|
|
47
56
|
declare function restorePurchases(): Promise<CustomerInfo>;
|
package/dist/index.d.ts
CHANGED
|
@@ -35,13 +35,22 @@ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "a
|
|
|
35
35
|
* Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
|
|
36
36
|
* and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Store bridges disagree about how the result comes back, so we accept it from any source and never
|
|
39
|
+
* hang waiting for one of them:
|
|
40
|
+
* 1. `purchaseUpdatedListener` — expo-iap's event-based flow.
|
|
41
|
+
* 2. `requestPurchase`'s resolved value — react-native-iap 15+ / Nitro resolves with the purchase
|
|
42
|
+
* and may never emit the event.
|
|
43
|
+
* 3. Reconciliation after `timeoutMs` — re-read `getAvailablePurchases()`, and failing that check
|
|
44
|
+
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
45
|
+
* grants it independently of the client).
|
|
46
|
+
*
|
|
47
|
+
* Rejects with "purchase-cancelled" if the user backs out, "purchase-timeout" if nothing arrived
|
|
48
|
+
* and nothing was granted.
|
|
41
49
|
*/
|
|
42
50
|
declare function purchaseProduct(productId: string, opts?: {
|
|
43
51
|
isSubscription?: boolean;
|
|
44
52
|
isConsumable?: boolean;
|
|
53
|
+
timeoutMs?: number;
|
|
45
54
|
}): Promise<CustomerInfo>;
|
|
46
55
|
/** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
|
|
47
56
|
declare function restorePurchases(): Promise<CustomerInfo>;
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ function iap() {
|
|
|
17
17
|
}
|
|
18
18
|
return _iap;
|
|
19
19
|
}
|
|
20
|
+
var RN_SDK_VERSION = "0.1.6";
|
|
20
21
|
var client;
|
|
21
22
|
function eh() {
|
|
22
23
|
if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
|
|
@@ -24,7 +25,12 @@ function eh() {
|
|
|
24
25
|
}
|
|
25
26
|
async function configureEntitleHub(opts) {
|
|
26
27
|
if (opts.iap) _iap = opts.iap;
|
|
27
|
-
client = new EntitleHub({
|
|
28
|
+
client = new EntitleHub({
|
|
29
|
+
apiKey: opts.apiKey,
|
|
30
|
+
appUserId: opts.appUserId,
|
|
31
|
+
baseUrl: opts.baseUrl,
|
|
32
|
+
client: `react-native/${RN_SDK_VERSION}`
|
|
33
|
+
});
|
|
28
34
|
try {
|
|
29
35
|
await iap().initConnection();
|
|
30
36
|
} catch {
|
|
@@ -45,10 +51,17 @@ async function getOfferings() {
|
|
|
45
51
|
async function getProducts(productIds, type = "all") {
|
|
46
52
|
return iap().fetchProducts({ skus: productIds, type });
|
|
47
53
|
}
|
|
54
|
+
var DEFAULT_PURCHASE_TIMEOUT_MS = 12e4;
|
|
48
55
|
async function purchaseProduct(productId, opts = {}) {
|
|
49
56
|
const I = iap();
|
|
50
57
|
const isSub = Boolean(opts.isSubscription);
|
|
51
58
|
const isConsumable = Boolean(opts.isConsumable) && !isSub;
|
|
59
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_PURCHASE_TIMEOUT_MS;
|
|
60
|
+
let entitlementsBefore = [];
|
|
61
|
+
try {
|
|
62
|
+
entitlementsBefore = (await eh().getCustomerInfo()).activeEntitlementIds;
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
52
65
|
const request = { apple: { sku: productId }, google: { skus: [productId] } };
|
|
53
66
|
if (isSub) {
|
|
54
67
|
try {
|
|
@@ -63,6 +76,7 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
63
76
|
}
|
|
64
77
|
return new Promise((resolve, reject) => {
|
|
65
78
|
let settled = false;
|
|
79
|
+
let timer;
|
|
66
80
|
const cleanup = () => {
|
|
67
81
|
try {
|
|
68
82
|
updSub?.remove();
|
|
@@ -72,8 +86,15 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
72
86
|
errSub?.remove();
|
|
73
87
|
} catch {
|
|
74
88
|
}
|
|
89
|
+
if (timer) clearTimeout(timer);
|
|
90
|
+
};
|
|
91
|
+
const fail = (e) => {
|
|
92
|
+
if (settled) return;
|
|
93
|
+
settled = true;
|
|
94
|
+
cleanup();
|
|
95
|
+
reject(e);
|
|
75
96
|
};
|
|
76
|
-
const
|
|
97
|
+
const settleWithPurchase = async (purchase) => {
|
|
77
98
|
if (settled) return;
|
|
78
99
|
settled = true;
|
|
79
100
|
cleanup();
|
|
@@ -87,23 +108,47 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
87
108
|
} catch (e) {
|
|
88
109
|
reject(e);
|
|
89
110
|
}
|
|
111
|
+
};
|
|
112
|
+
const isPurchase = (p) => Boolean(p) && Boolean(p.purchaseToken || p.jwsRepresentation || p.jwsRepresentationIos || p.purchaseTokenAndroid || p.transactionId || p.transactionReceipt || p.id);
|
|
113
|
+
const reconcile = async () => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
try {
|
|
116
|
+
const owned = await I.getAvailablePurchases() || [];
|
|
117
|
+
const match = owned.find((p) => (p?.productId ?? p?.id ?? p?.sku) === productId);
|
|
118
|
+
if (match) return settleWithPurchase(match);
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const info = await eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
123
|
+
const grew = info.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id));
|
|
124
|
+
if (grew) {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
cleanup();
|
|
128
|
+
resolve(info);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
fail(new Error("purchase-timeout"));
|
|
134
|
+
};
|
|
135
|
+
const updSub = I.purchaseUpdatedListener((purchase) => {
|
|
136
|
+
void settleWithPurchase(purchase);
|
|
90
137
|
});
|
|
91
138
|
const errSub = I.purchaseErrorListener((error) => {
|
|
92
|
-
if (settled) return;
|
|
93
|
-
settled = true;
|
|
94
|
-
cleanup();
|
|
95
139
|
const code = error?.code;
|
|
96
140
|
const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
|
|
97
|
-
|
|
141
|
+
fail(new Error(cancelled ? "purchase-cancelled" : error?.message || "purchase-failed"));
|
|
98
142
|
});
|
|
99
143
|
Promise.resolve(
|
|
100
144
|
I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
|
|
101
|
-
).
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
).then((res) => {
|
|
146
|
+
const p = Array.isArray(res) ? res.find(isPurchase) : res;
|
|
147
|
+
if (isPurchase(p)) void settleWithPurchase(p);
|
|
148
|
+
}).catch((e) => fail(e));
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
void reconcile();
|
|
151
|
+
}, timeoutMs);
|
|
107
152
|
});
|
|
108
153
|
}
|
|
109
154
|
async function restorePurchases() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entitlehub/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|