@entitlehub/react-native 0.1.5 → 0.1.7
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 +27 -0
- package/dist/index.cjs +68 -25
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +67 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7 — 2026-07-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **A completed purchase could still be reported as failed.** After a timeout, `purchaseProduct()`
|
|
7
|
+
checked for confirmation exactly once. Store notifications are asynchronous, so that single
|
|
8
|
+
reading races the grant and sometimes loses — observed live: the entitlement was written seconds
|
|
9
|
+
after the check ran, and the user saw a purchase-failed error for a purchase that worked. It now
|
|
10
|
+
polls the store and the server with backoff (`reconcileMs`, default 30s) before giving up.
|
|
11
|
+
- **The timeout counted the user's own time.** The timer was armed when `purchaseProduct()` was
|
|
12
|
+
called — before the store's password / Face ID sheet appears — so a user who paused mid-purchase
|
|
13
|
+
tripped it. It now starts once the request is actually in flight, with a separate generous
|
|
14
|
+
backstop (`sheetTimeoutMs`, default 10m) so a stuck bridge still can't hang forever.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Rejections are now typed `PurchaseError`s — branch on `.code`, not the message.**
|
|
18
|
+
`purchase-cancelled`, `purchase-failed`, and the new `purchase-pending`. **`purchase-pending` is
|
|
19
|
+
not a failure**: the store took the money but hasn't confirmed yet. Show "still confirming" — the
|
|
20
|
+
entitlement lands on its own and `addCustomerInfoUpdateListener` fires when it does. `.pending`
|
|
21
|
+
and `.customerInfo` are on the error. The old untyped `"purchase-timeout"` message is gone.
|
|
22
|
+
|
|
23
|
+
## 0.1.6 — 2026-07-21
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Reports `react-native/<version>` to EntitleHub on each call, so the dashboard can tell you when
|
|
27
|
+
your SDK is behind a release with fixes in it — rather than you finding out by hitting the bug.
|
|
28
|
+
Version only; no device or user data.
|
|
29
|
+
|
|
3
30
|
## 0.1.5 — 2026-07-21
|
|
4
31
|
|
|
5
32
|
### Fixed
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
PurchaseError: () => PurchaseError,
|
|
23
24
|
addCustomerInfoUpdateListener: () => addCustomerInfoUpdateListener,
|
|
24
25
|
configureEntitleHub: () => configureEntitleHub,
|
|
25
26
|
getCustomerInfo: () => getCustomerInfo,
|
|
@@ -42,6 +43,7 @@ function iap() {
|
|
|
42
43
|
}
|
|
43
44
|
return _iap;
|
|
44
45
|
}
|
|
46
|
+
var RN_SDK_VERSION = "0.1.7";
|
|
45
47
|
var client;
|
|
46
48
|
function eh() {
|
|
47
49
|
if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
|
|
@@ -49,7 +51,12 @@ function eh() {
|
|
|
49
51
|
}
|
|
50
52
|
async function configureEntitleHub(opts) {
|
|
51
53
|
if (opts.iap) _iap = opts.iap;
|
|
52
|
-
client = new import_sdk.EntitleHub({
|
|
54
|
+
client = new import_sdk.EntitleHub({
|
|
55
|
+
apiKey: opts.apiKey,
|
|
56
|
+
appUserId: opts.appUserId,
|
|
57
|
+
baseUrl: opts.baseUrl,
|
|
58
|
+
client: `react-native/${RN_SDK_VERSION}`
|
|
59
|
+
});
|
|
53
60
|
try {
|
|
54
61
|
await iap().initConnection();
|
|
55
62
|
} catch {
|
|
@@ -71,11 +78,25 @@ async function getProducts(productIds, type = "all") {
|
|
|
71
78
|
return iap().fetchProducts({ skus: productIds, type });
|
|
72
79
|
}
|
|
73
80
|
var DEFAULT_PURCHASE_TIMEOUT_MS = 12e4;
|
|
81
|
+
var DEFAULT_SHEET_TIMEOUT_MS = 6e5;
|
|
82
|
+
var DEFAULT_RECONCILE_MS = 3e4;
|
|
83
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
84
|
+
var PurchaseError = class extends Error {
|
|
85
|
+
constructor(code, message, customerInfo) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "PurchaseError";
|
|
88
|
+
this.code = code;
|
|
89
|
+
this.pending = code === "purchase-pending";
|
|
90
|
+
this.customerInfo = customerInfo;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
74
93
|
async function purchaseProduct(productId, opts = {}) {
|
|
75
94
|
const I = iap();
|
|
76
95
|
const isSub = Boolean(opts.isSubscription);
|
|
77
96
|
const isConsumable = Boolean(opts.isConsumable) && !isSub;
|
|
78
97
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_PURCHASE_TIMEOUT_MS;
|
|
98
|
+
const sheetTimeoutMs = opts.sheetTimeoutMs ?? DEFAULT_SHEET_TIMEOUT_MS;
|
|
99
|
+
const reconcileMs = opts.reconcileMs ?? DEFAULT_RECONCILE_MS;
|
|
79
100
|
let entitlementsBefore = [];
|
|
80
101
|
try {
|
|
81
102
|
entitlementsBefore = (await eh().getCustomerInfo()).activeEntitlementIds;
|
|
@@ -130,26 +151,43 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
130
151
|
};
|
|
131
152
|
const isPurchase = (p) => Boolean(p) && Boolean(p.purchaseToken || p.jwsRepresentation || p.jwsRepresentationIos || p.purchaseTokenAndroid || p.transactionId || p.transactionReceipt || p.id);
|
|
132
153
|
const reconcile = async () => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const grew = info.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id));
|
|
143
|
-
if (grew) {
|
|
144
|
-
if (settled) return;
|
|
145
|
-
settled = true;
|
|
146
|
-
cleanup();
|
|
147
|
-
resolve(info);
|
|
148
|
-
return;
|
|
154
|
+
const deadline = Date.now() + reconcileMs;
|
|
155
|
+
let wait = 2e3;
|
|
156
|
+
let lastInfo;
|
|
157
|
+
while (!settled) {
|
|
158
|
+
try {
|
|
159
|
+
const owned = await I.getAvailablePurchases() || [];
|
|
160
|
+
const match = owned.find((p) => (p?.productId ?? p?.id ?? p?.sku) === productId);
|
|
161
|
+
if (match) return settleWithPurchase(match);
|
|
162
|
+
} catch {
|
|
149
163
|
}
|
|
150
|
-
|
|
164
|
+
try {
|
|
165
|
+
lastInfo = await eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
166
|
+
if (lastInfo.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id))) {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
cleanup();
|
|
170
|
+
resolve(lastInfo);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
const remaining = deadline - Date.now();
|
|
176
|
+
if (remaining <= 0) break;
|
|
177
|
+
await sleep(Math.min(wait, remaining));
|
|
178
|
+
wait = Math.min(wait * 1.5, 8e3);
|
|
151
179
|
}
|
|
152
|
-
fail(new
|
|
180
|
+
fail(new PurchaseError(
|
|
181
|
+
"purchase-pending",
|
|
182
|
+
"The store hasn't confirmed this purchase yet. It may still complete \u2014 show a pending state, not a failure.",
|
|
183
|
+
lastInfo
|
|
184
|
+
));
|
|
185
|
+
};
|
|
186
|
+
const armTimer = (ms) => {
|
|
187
|
+
if (timer) clearTimeout(timer);
|
|
188
|
+
timer = setTimeout(() => {
|
|
189
|
+
void reconcile();
|
|
190
|
+
}, ms);
|
|
153
191
|
};
|
|
154
192
|
const updSub = I.purchaseUpdatedListener((purchase) => {
|
|
155
193
|
void settleWithPurchase(purchase);
|
|
@@ -157,17 +195,21 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
157
195
|
const errSub = I.purchaseErrorListener((error) => {
|
|
158
196
|
const code = error?.code;
|
|
159
197
|
const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
|
|
160
|
-
fail(
|
|
198
|
+
fail(cancelled ? new PurchaseError("purchase-cancelled", "The user cancelled the purchase.") : new PurchaseError("purchase-failed", error?.message || "The purchase failed."));
|
|
161
199
|
});
|
|
162
200
|
Promise.resolve(
|
|
163
201
|
I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
|
|
164
202
|
).then((res) => {
|
|
165
203
|
const p = Array.isArray(res) ? res.find(isPurchase) : res;
|
|
166
|
-
if (isPurchase(p))
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
204
|
+
if (isPurchase(p)) {
|
|
205
|
+
void settleWithPurchase(p);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
armTimer(timeoutMs);
|
|
209
|
+
}).catch((e) => fail(
|
|
210
|
+
e instanceof PurchaseError ? e : new PurchaseError("purchase-failed", e?.message || "The purchase failed.")
|
|
211
|
+
));
|
|
212
|
+
armTimer(sheetTimeoutMs);
|
|
171
213
|
});
|
|
172
214
|
}
|
|
173
215
|
async function restorePurchases() {
|
|
@@ -198,6 +240,7 @@ async function reportToEntitleHub(purchase, productId, isSubscription) {
|
|
|
198
240
|
}
|
|
199
241
|
// Annotate the CommonJS export names for ESM import in node:
|
|
200
242
|
0 && (module.exports = {
|
|
243
|
+
PurchaseError,
|
|
201
244
|
addCustomerInfoUpdateListener,
|
|
202
245
|
configureEntitleHub,
|
|
203
246
|
getCustomerInfo,
|
package/dist/index.d.cts
CHANGED
|
@@ -31,6 +31,23 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
|
|
|
31
31
|
declare function getOfferings(): Promise<Offerings>;
|
|
32
32
|
/** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
|
|
33
33
|
declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
|
|
34
|
+
/** Why a purchase didn't complete. Check `code` — never match on the message. */
|
|
35
|
+
type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
|
|
36
|
+
/**
|
|
37
|
+
* A purchase that didn't return entitlements.
|
|
38
|
+
*
|
|
39
|
+
* `code === "purchase-pending"` is NOT a failure: the store took the money but confirmation hasn't
|
|
40
|
+
* reached us yet (store notifications are asynchronous). Show "still confirming", not an error —
|
|
41
|
+
* the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
|
|
42
|
+
*/
|
|
43
|
+
declare class PurchaseError extends Error {
|
|
44
|
+
readonly code: PurchaseErrorCode;
|
|
45
|
+
/** True when the purchase probably succeeded and is just unconfirmed. */
|
|
46
|
+
readonly pending: boolean;
|
|
47
|
+
/** Last known entitlements, if we managed to read them. */
|
|
48
|
+
readonly customerInfo?: CustomerInfo;
|
|
49
|
+
constructor(code: PurchaseErrorCode, message: string, customerInfo?: CustomerInfo);
|
|
50
|
+
}
|
|
34
51
|
/**
|
|
35
52
|
* Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
|
|
36
53
|
* and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
|
|
@@ -44,17 +61,25 @@ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "a
|
|
|
44
61
|
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
45
62
|
* grants it independently of the client).
|
|
46
63
|
*
|
|
47
|
-
* Rejects with
|
|
48
|
-
*
|
|
64
|
+
* Rejects with a `PurchaseError`; branch on `.code`, never the message:
|
|
65
|
+
* - `purchase-cancelled` — the user backed out.
|
|
66
|
+
* - `purchase-pending` — the store took the money but hasn't confirmed. NOT a failure; show a
|
|
67
|
+
* pending state. `.pending` is true and the entitlement will land on its own.
|
|
68
|
+
* - `purchase-failed` — a real failure.
|
|
49
69
|
*/
|
|
50
70
|
declare function purchaseProduct(productId: string, opts?: {
|
|
51
71
|
isSubscription?: boolean;
|
|
52
72
|
isConsumable?: boolean;
|
|
73
|
+
/** Wait for the store's result once the request is in flight (default 120s). */
|
|
53
74
|
timeoutMs?: number;
|
|
75
|
+
/** Absolute backstop from the call, covering time spent in the store's own sheet (default 10m). */
|
|
76
|
+
sheetTimeoutMs?: number;
|
|
77
|
+
/** How long to poll for confirmation before reporting pending (default 30s). */
|
|
78
|
+
reconcileMs?: number;
|
|
54
79
|
}): Promise<CustomerInfo>;
|
|
55
80
|
/** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
|
|
56
81
|
declare function restorePurchases(): Promise<CustomerInfo>;
|
|
57
82
|
/** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
|
|
58
83
|
declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
|
|
59
84
|
|
|
60
|
-
export { type ConfigureOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
|
85
|
+
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,23 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
|
|
|
31
31
|
declare function getOfferings(): Promise<Offerings>;
|
|
32
32
|
/** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
|
|
33
33
|
declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
|
|
34
|
+
/** Why a purchase didn't complete. Check `code` — never match on the message. */
|
|
35
|
+
type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
|
|
36
|
+
/**
|
|
37
|
+
* A purchase that didn't return entitlements.
|
|
38
|
+
*
|
|
39
|
+
* `code === "purchase-pending"` is NOT a failure: the store took the money but confirmation hasn't
|
|
40
|
+
* reached us yet (store notifications are asynchronous). Show "still confirming", not an error —
|
|
41
|
+
* the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
|
|
42
|
+
*/
|
|
43
|
+
declare class PurchaseError extends Error {
|
|
44
|
+
readonly code: PurchaseErrorCode;
|
|
45
|
+
/** True when the purchase probably succeeded and is just unconfirmed. */
|
|
46
|
+
readonly pending: boolean;
|
|
47
|
+
/** Last known entitlements, if we managed to read them. */
|
|
48
|
+
readonly customerInfo?: CustomerInfo;
|
|
49
|
+
constructor(code: PurchaseErrorCode, message: string, customerInfo?: CustomerInfo);
|
|
50
|
+
}
|
|
34
51
|
/**
|
|
35
52
|
* Buy a product in one call: open the native purchase sheet, validate the receipt with EntitleHub,
|
|
36
53
|
* and return the updated entitlements. Purchase + entitlement sync, the RevenueCat way.
|
|
@@ -44,17 +61,25 @@ declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "a
|
|
|
44
61
|
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
45
62
|
* grants it independently of the client).
|
|
46
63
|
*
|
|
47
|
-
* Rejects with
|
|
48
|
-
*
|
|
64
|
+
* Rejects with a `PurchaseError`; branch on `.code`, never the message:
|
|
65
|
+
* - `purchase-cancelled` — the user backed out.
|
|
66
|
+
* - `purchase-pending` — the store took the money but hasn't confirmed. NOT a failure; show a
|
|
67
|
+
* pending state. `.pending` is true and the entitlement will land on its own.
|
|
68
|
+
* - `purchase-failed` — a real failure.
|
|
49
69
|
*/
|
|
50
70
|
declare function purchaseProduct(productId: string, opts?: {
|
|
51
71
|
isSubscription?: boolean;
|
|
52
72
|
isConsumable?: boolean;
|
|
73
|
+
/** Wait for the store's result once the request is in flight (default 120s). */
|
|
53
74
|
timeoutMs?: number;
|
|
75
|
+
/** Absolute backstop from the call, covering time spent in the store's own sheet (default 10m). */
|
|
76
|
+
sheetTimeoutMs?: number;
|
|
77
|
+
/** How long to poll for confirmation before reporting pending (default 30s). */
|
|
78
|
+
reconcileMs?: number;
|
|
54
79
|
}): Promise<CustomerInfo>;
|
|
55
80
|
/** Restore the user's purchases: re-validate each with EntitleHub, then return entitlements. */
|
|
56
81
|
declare function restorePurchases(): Promise<CustomerInfo>;
|
|
57
82
|
/** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
|
|
58
83
|
declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
|
|
59
84
|
|
|
60
|
-
export { type ConfigureOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
|
85
|
+
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
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.7";
|
|
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 {
|
|
@@ -46,11 +52,25 @@ async function getProducts(productIds, type = "all") {
|
|
|
46
52
|
return iap().fetchProducts({ skus: productIds, type });
|
|
47
53
|
}
|
|
48
54
|
var DEFAULT_PURCHASE_TIMEOUT_MS = 12e4;
|
|
55
|
+
var DEFAULT_SHEET_TIMEOUT_MS = 6e5;
|
|
56
|
+
var DEFAULT_RECONCILE_MS = 3e4;
|
|
57
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
58
|
+
var PurchaseError = class extends Error {
|
|
59
|
+
constructor(code, message, customerInfo) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "PurchaseError";
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.pending = code === "purchase-pending";
|
|
64
|
+
this.customerInfo = customerInfo;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
49
67
|
async function purchaseProduct(productId, opts = {}) {
|
|
50
68
|
const I = iap();
|
|
51
69
|
const isSub = Boolean(opts.isSubscription);
|
|
52
70
|
const isConsumable = Boolean(opts.isConsumable) && !isSub;
|
|
53
71
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_PURCHASE_TIMEOUT_MS;
|
|
72
|
+
const sheetTimeoutMs = opts.sheetTimeoutMs ?? DEFAULT_SHEET_TIMEOUT_MS;
|
|
73
|
+
const reconcileMs = opts.reconcileMs ?? DEFAULT_RECONCILE_MS;
|
|
54
74
|
let entitlementsBefore = [];
|
|
55
75
|
try {
|
|
56
76
|
entitlementsBefore = (await eh().getCustomerInfo()).activeEntitlementIds;
|
|
@@ -105,26 +125,43 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
105
125
|
};
|
|
106
126
|
const isPurchase = (p) => Boolean(p) && Boolean(p.purchaseToken || p.jwsRepresentation || p.jwsRepresentationIos || p.purchaseTokenAndroid || p.transactionId || p.transactionReceipt || p.id);
|
|
107
127
|
const reconcile = async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const grew = info.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id));
|
|
118
|
-
if (grew) {
|
|
119
|
-
if (settled) return;
|
|
120
|
-
settled = true;
|
|
121
|
-
cleanup();
|
|
122
|
-
resolve(info);
|
|
123
|
-
return;
|
|
128
|
+
const deadline = Date.now() + reconcileMs;
|
|
129
|
+
let wait = 2e3;
|
|
130
|
+
let lastInfo;
|
|
131
|
+
while (!settled) {
|
|
132
|
+
try {
|
|
133
|
+
const owned = await I.getAvailablePurchases() || [];
|
|
134
|
+
const match = owned.find((p) => (p?.productId ?? p?.id ?? p?.sku) === productId);
|
|
135
|
+
if (match) return settleWithPurchase(match);
|
|
136
|
+
} catch {
|
|
124
137
|
}
|
|
125
|
-
|
|
138
|
+
try {
|
|
139
|
+
lastInfo = await eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
140
|
+
if (lastInfo.activeEntitlementIds.some((id) => !entitlementsBefore.includes(id))) {
|
|
141
|
+
if (settled) return;
|
|
142
|
+
settled = true;
|
|
143
|
+
cleanup();
|
|
144
|
+
resolve(lastInfo);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
const remaining = deadline - Date.now();
|
|
150
|
+
if (remaining <= 0) break;
|
|
151
|
+
await sleep(Math.min(wait, remaining));
|
|
152
|
+
wait = Math.min(wait * 1.5, 8e3);
|
|
126
153
|
}
|
|
127
|
-
fail(new
|
|
154
|
+
fail(new PurchaseError(
|
|
155
|
+
"purchase-pending",
|
|
156
|
+
"The store hasn't confirmed this purchase yet. It may still complete \u2014 show a pending state, not a failure.",
|
|
157
|
+
lastInfo
|
|
158
|
+
));
|
|
159
|
+
};
|
|
160
|
+
const armTimer = (ms) => {
|
|
161
|
+
if (timer) clearTimeout(timer);
|
|
162
|
+
timer = setTimeout(() => {
|
|
163
|
+
void reconcile();
|
|
164
|
+
}, ms);
|
|
128
165
|
};
|
|
129
166
|
const updSub = I.purchaseUpdatedListener((purchase) => {
|
|
130
167
|
void settleWithPurchase(purchase);
|
|
@@ -132,17 +169,21 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
132
169
|
const errSub = I.purchaseErrorListener((error) => {
|
|
133
170
|
const code = error?.code;
|
|
134
171
|
const cancelled = code === "E_USER_CANCELLED" || code === "user-cancelled" || /cancel/i.test(String(error?.message));
|
|
135
|
-
fail(
|
|
172
|
+
fail(cancelled ? new PurchaseError("purchase-cancelled", "The user cancelled the purchase.") : new PurchaseError("purchase-failed", error?.message || "The purchase failed."));
|
|
136
173
|
});
|
|
137
174
|
Promise.resolve(
|
|
138
175
|
I.requestPurchase({ request, type: isSub ? "subs" : "in-app" })
|
|
139
176
|
).then((res) => {
|
|
140
177
|
const p = Array.isArray(res) ? res.find(isPurchase) : res;
|
|
141
|
-
if (isPurchase(p))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
if (isPurchase(p)) {
|
|
179
|
+
void settleWithPurchase(p);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
armTimer(timeoutMs);
|
|
183
|
+
}).catch((e) => fail(
|
|
184
|
+
e instanceof PurchaseError ? e : new PurchaseError("purchase-failed", e?.message || "The purchase failed.")
|
|
185
|
+
));
|
|
186
|
+
armTimer(sheetTimeoutMs);
|
|
146
187
|
});
|
|
147
188
|
}
|
|
148
189
|
async function restorePurchases() {
|
|
@@ -172,6 +213,7 @@ async function reportToEntitleHub(purchase, productId, isSubscription) {
|
|
|
172
213
|
throw new Error("Could not read a receipt from the purchase (no iOS JWS or Android purchase token).");
|
|
173
214
|
}
|
|
174
215
|
export {
|
|
216
|
+
PurchaseError,
|
|
175
217
|
addCustomerInfoUpdateListener,
|
|
176
218
|
configureEntitleHub,
|
|
177
219
|
getCustomerInfo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entitlehub/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|