@entitlehub/react-native 0.1.7 → 0.1.9
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 +63 -15
- package/README.md +11 -8
- package/dist/index.cjs +34 -7
- package/dist/index.d.cts +27 -15
- package/dist/index.d.ts +27 -15
- package/dist/index.js +23 -14
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.9 (2026-09-20)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`restorePurchases()` swallowed every per-purchase failure.** Each purchase was reported with
|
|
7
|
+
`.catch(() => {})`, so a `409 receipt_revoked` (the signal that a purchase was REFUNDED) died
|
|
8
|
+
inside the SDK and no amount of catching in the app could see it. A restore is the main place a
|
|
9
|
+
refund surfaces, so this was the path that mattered most. Failures now reach an optional
|
|
10
|
+
`onReportError`, and `console.warn` otherwise.
|
|
11
|
+
|
|
12
|
+
### Read this before upgrading
|
|
13
|
+
- **If you were catching around `restorePurchases()` to detect a refund, that never worked and
|
|
14
|
+
still will not.** The loop deliberately continues past a bad receipt so one stale purchase can't
|
|
15
|
+
cost someone the rest of theirs, which means `restorePurchases()` **resolves** rather than
|
|
16
|
+
rejecting. Pass `onReportError` - it is the only way to see these:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { restorePurchases, isReceiptRevoked } from "@entitlehub/react-native";
|
|
20
|
+
|
|
21
|
+
const info = await restorePurchases({
|
|
22
|
+
onReportError: (err) => { if (isReceiptRevoked(err)) showRefundNotice(); },
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Passing the options object to **0.1.7/0.1.8 is harmless** (JS drops the extra argument), so it is
|
|
27
|
+
safe to add before you upgrade.
|
|
28
|
+
- `isReceiptRevoked` is re-exported here, so you don't need to import `@entitlehub/sdk` for it.
|
|
29
|
+
It is a **0.3.1** symbol, so the peer range moves to `>=0.3.1`. On an older core SDK the
|
|
30
|
+
re-export resolves to `undefined` rather than failing loudly, which is the whole reason the
|
|
31
|
+
range had to move: `>=0.2.0` would have permitted a version that cannot satisfy it.
|
|
32
|
+
|
|
33
|
+
## 0.1.8 (2026-09-19)
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Black screen on launch: "Requiring unknown module expo-iap".** The SDK loaded `expo-iap` with
|
|
37
|
+
`require()`. Metro decides what to bundle by reading literal `require("…")` / `import("…")` calls,
|
|
38
|
+
and the ESM build rewrote that call into esbuild's `__require` shim, which Metro cannot see. So
|
|
39
|
+
`expo-iap` was never put in the bundle, the first call threw, and React Native tore the UI down
|
|
40
|
+
with it, before the store sheet ever opened. It is now loaded with `import("expo-iap")`, which
|
|
41
|
+
survives the build as a literal. Verified by bundling a real Expo app: with 0.1.7 the store
|
|
42
|
+
library is absent from the bundle, with 0.1.8 it is there.
|
|
43
|
+
- The module now loads while `configureEntitleHub()` runs, so a missing or broken `expo-iap` raises
|
|
44
|
+
a clear error there instead of being swallowed by the store-connection catch, and handles CJS
|
|
45
|
+
interop (module namespace or `.default`).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- `configureEntitleHub({ iap })` is no longer a workaround for the bug above, just the way to pin an
|
|
49
|
+
exact module instance or use another OpenIAP-compatible library.
|
|
50
|
+
|
|
51
|
+
## 0.1.7 (2026-07-22)
|
|
4
52
|
|
|
5
53
|
### Fixed
|
|
6
54
|
- **A completed purchase could still be reported as failed.** After a timeout, `purchaseProduct()`
|
|
7
55
|
checked for confirmation exactly once. Store notifications are asynchronous, so that single
|
|
8
|
-
reading races the grant and sometimes loses
|
|
56
|
+
reading races the grant and sometimes loses, observed live: the entitlement was written seconds
|
|
9
57
|
after the check ran, and the user saw a purchase-failed error for a purchase that worked. It now
|
|
10
58
|
polls the store and the server with backoff (`reconcileMs`, default 30s) before giving up.
|
|
11
59
|
- **The timeout counted the user's own time.** The timer was armed when `purchaseProduct()` was
|
|
12
|
-
called
|
|
60
|
+
called: before the store's password / Face ID sheet appears, so a user who paused mid-purchase
|
|
13
61
|
tripped it. It now starts once the request is actually in flight, with a separate generous
|
|
14
62
|
backstop (`sheetTimeoutMs`, default 10m) so a stuck bridge still can't hang forever.
|
|
15
63
|
|
|
16
64
|
### Changed
|
|
17
|
-
- **Rejections are now typed `PurchaseError`s
|
|
65
|
+
- **Rejections are now typed `PurchaseError`s: branch on `.code`, not the message.**
|
|
18
66
|
`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"
|
|
67
|
+
not a failure**: the store took the money but hasn't confirmed yet. Show "still confirming", the
|
|
20
68
|
entitlement lands on its own and `addCustomerInfoUpdateListener` fires when it does. `.pending`
|
|
21
69
|
and `.customerInfo` are on the error. The old untyped `"purchase-timeout"` message is gone.
|
|
22
70
|
|
|
23
|
-
## 0.1.6
|
|
71
|
+
## 0.1.6 (2026-07-21)
|
|
24
72
|
|
|
25
73
|
### Added
|
|
26
74
|
- 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
|
|
75
|
+
your SDK is behind a release with fixes in it, rather than you finding out by hitting the bug.
|
|
28
76
|
Version only; no device or user data.
|
|
29
77
|
|
|
30
|
-
## 0.1.5
|
|
78
|
+
## 0.1.5 (2026-07-21)
|
|
31
79
|
|
|
32
80
|
### Fixed
|
|
33
81
|
- **`purchaseProduct()` could hang forever after a successful purchase.** It waited only on
|
|
34
82
|
`purchaseUpdatedListener`; on the react-native-iap 15+ / Nitro bridge that callback never fires,
|
|
35
|
-
so the promise never settled
|
|
83
|
+
so the promise never settled: the paywall stayed open and features stayed locked even though the
|
|
36
84
|
user had been charged and EntitleHub had already granted the entitlement server-side.
|
|
37
85
|
`requestPurchase`'s resolved value was being discarded entirely; it is now honoured, so bridges
|
|
38
86
|
that resolve instead of emitting an event work.
|
|
@@ -40,14 +88,14 @@
|
|
|
40
88
|
### Added
|
|
41
89
|
- Reconciliation safety net: if no result arrives within `timeoutMs` (default 120s),
|
|
42
90
|
`purchaseProduct()` re-reads `getAvailablePurchases()` and, failing that, re-checks entitlements
|
|
43
|
-
from the server
|
|
91
|
+
from the server: resolving successfully if the entitlement appeared while the client was waiting.
|
|
44
92
|
Only rejects (`"purchase-timeout"`) when nothing was actually granted.
|
|
45
93
|
- `purchaseProduct(id, { timeoutMs })` to tune that window.
|
|
46
94
|
|
|
47
|
-
## 0.1.2
|
|
95
|
+
## 0.1.2 (2026-07-18)
|
|
48
96
|
|
|
49
97
|
### Added
|
|
50
|
-
- `configureEntitleHub({ iap })
|
|
98
|
+
- `configureEntitleHub({ iap })`: inject the OpenIAP-compatible store module (pinned expo-iap
|
|
51
99
|
version, or react-native-iap) instead of the hard-coded `expo-iap`. Decouples this SDK from a
|
|
52
100
|
single store-library version so you can work around a native issue without changing the SDK.
|
|
53
101
|
|
|
@@ -56,17 +104,17 @@
|
|
|
56
104
|
thread) are a store-library-version concern; how to pin a version, inject a module, or fall back
|
|
57
105
|
to the fully-decoupled `@entitlehub/sdk` + your own billing library.
|
|
58
106
|
|
|
59
|
-
## 0.1.1
|
|
107
|
+
## 0.1.1 (2026-07-18)
|
|
60
108
|
|
|
61
109
|
### Fixed
|
|
62
|
-
- **`purchaseProduct` crashed
|
|
110
|
+
- **`purchaseProduct` crashed**: 0.1.0 was written against expo-iap's old flat `requestPurchase`
|
|
63
111
|
API and read the purchase from the return value. `expo-iap@4.x` is **event-based** with a
|
|
64
112
|
discriminated request. Now: `requestPurchase({ request: { apple: { sku }, google: { skus } },
|
|
65
113
|
type })` with the result bridged from `purchaseUpdatedListener` / `purchaseErrorListener`.
|
|
66
114
|
- Android subscriptions now pass the required `offerToken` (fetched from the product).
|
|
67
115
|
- `getProducts` uses expo-iap's `fetchProducts` (the old `getProducts` no longer exists).
|
|
68
116
|
|
|
69
|
-
## 0.1.0
|
|
117
|
+
## 0.1.0 (2026-07-18)
|
|
70
118
|
|
|
71
119
|
Initial release.
|
|
72
120
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@entitlehub/react-native)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
EntitleHub for **React Native & Expo
|
|
6
|
+
EntitleHub for **React Native & Expo**: one call to purchase and unlock entitlements. It opens the
|
|
7
7
|
native store sheet (via [`expo-iap`](https://github.com/hyochan/expo-iap)), validates the receipt
|
|
8
8
|
with EntitleHub server-side, and hands you the updated entitlements. The RevenueCat developer
|
|
9
9
|
experience, on EntitleHub.
|
|
@@ -26,7 +26,7 @@ await configureEntitleHub({ apiKey: "pk_live_…", appUserId: user.id });
|
|
|
26
26
|
// gate a feature:
|
|
27
27
|
if (await isEntitled("pro")) unlockPro();
|
|
28
28
|
|
|
29
|
-
// buy
|
|
29
|
+
// buy: opens the native sheet, validates, returns entitlements:
|
|
30
30
|
const info = await purchaseProduct("app_pro_monthly", { isSubscription: true });
|
|
31
31
|
if (info.isActive("pro")) unlockPro();
|
|
32
32
|
|
|
@@ -45,7 +45,7 @@ const restored = await restorePurchases();
|
|
|
45
45
|
| `addCustomerInfoUpdateListener(fn)` | React to entitlement changes. |
|
|
46
46
|
|
|
47
47
|
Only your **publishable** key (`pk_`) ships in the app. EntitleHub validates the Apple/Google
|
|
48
|
-
receipt server-side, so a forged one is rejected
|
|
48
|
+
receipt server-side, so a forged one is rejected, no secret key on the device.
|
|
49
49
|
|
|
50
50
|
## How it works
|
|
51
51
|
|
|
@@ -60,22 +60,25 @@ Full guide: **[entitlehub.com/docs/purchases](https://entitlehub.com/docs/purcha
|
|
|
60
60
|
## Choosing / pinning the store library
|
|
61
61
|
|
|
62
62
|
The on-device purchase runs through **expo-iap** (OpenIAP). That native layer is the store
|
|
63
|
-
library's responsibility, not EntitleHub's
|
|
63
|
+
library's responsibility, not EntitleHub's, and a given version can have platform-specific native
|
|
64
64
|
bugs. If you hit a **native IAP crash** (e.g. an `EXC_BAD_ACCESS` in the store module), it's the
|
|
65
65
|
store library, not this SDK or your app code, and no JS change can catch a native exception thrown
|
|
66
66
|
off the JS thread.
|
|
67
67
|
|
|
68
68
|
Two levers, both without changing this SDK:
|
|
69
69
|
|
|
70
|
-
- **Pin a working version:** `npm install expo-iap@<version
|
|
70
|
+
- **Pin a working version:** `npm install expo-iap@<version>`, this package uses whatever `expo-iap`
|
|
71
71
|
is installed (peer dependency).
|
|
72
|
-
- **Supply your own module:** pass any OpenIAP-compatible library to `configureEntitleHub
|
|
72
|
+
- **Supply your own module:** pass any OpenIAP-compatible library to `configureEntitleHub`. You do
|
|
73
|
+
not need this to use `expo-iap`, which is loaded for you; it is for pinning an exact instance or
|
|
74
|
+
using a different library:
|
|
73
75
|
```ts
|
|
74
|
-
|
|
76
|
+
import * as iap from "expo-iap"; // or react-native-iap
|
|
77
|
+
await configureEntitleHub({ apiKey, appUserId, iap });
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
Fully decoupled fallback: skip this package and use **[`@entitlehub/sdk`](https://www.npmjs.com/package/@entitlehub/sdk)**
|
|
78
|
-
directly
|
|
81
|
+
directly: open the purchase with *any* billing library you've verified on your target OS, then call
|
|
79
82
|
`eh.reportPurchase({ signedTransaction | purchaseToken })`. See
|
|
80
83
|
[Purchases → manual](https://entitlehub.com/docs/purchases).
|
|
81
84
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -27,23 +37,34 @@ __export(index_exports, {
|
|
|
27
37
|
getOfferings: () => getOfferings,
|
|
28
38
|
getProducts: () => getProducts,
|
|
29
39
|
isEntitled: () => isEntitled,
|
|
40
|
+
isReceiptRevoked: () => import_sdk2.isReceiptRevoked,
|
|
30
41
|
logIn: () => logIn,
|
|
31
42
|
purchaseProduct: () => purchaseProduct,
|
|
32
43
|
restorePurchases: () => restorePurchases
|
|
33
44
|
});
|
|
34
45
|
module.exports = __toCommonJS(index_exports);
|
|
35
46
|
var import_sdk = require("@entitlehub/sdk");
|
|
47
|
+
var import_sdk2 = require("@entitlehub/sdk");
|
|
36
48
|
var _iap;
|
|
37
|
-
function
|
|
49
|
+
async function loadIap() {
|
|
38
50
|
if (_iap) return _iap;
|
|
51
|
+
let mod;
|
|
39
52
|
try {
|
|
40
|
-
|
|
53
|
+
mod = await import("expo-iap");
|
|
41
54
|
} catch {
|
|
42
55
|
throw new Error("@entitlehub/react-native needs 'expo-iap'. Install it with: npx expo install expo-iap");
|
|
43
56
|
}
|
|
57
|
+
_iap = typeof mod?.initConnection === "function" ? mod : mod?.default ?? mod;
|
|
58
|
+
if (typeof _iap?.initConnection !== "function") {
|
|
59
|
+
throw new Error("@entitlehub/react-native: the 'expo-iap' module did not load correctly (no initConnection). Check that expo-iap is installed and its version supports the OpenIAP API.");
|
|
60
|
+
}
|
|
61
|
+
return _iap;
|
|
62
|
+
}
|
|
63
|
+
function iap() {
|
|
64
|
+
if (!_iap) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub purchases.");
|
|
44
65
|
return _iap;
|
|
45
66
|
}
|
|
46
|
-
var RN_SDK_VERSION = "0.1.
|
|
67
|
+
var RN_SDK_VERSION = "0.1.8";
|
|
47
68
|
var client;
|
|
48
69
|
function eh() {
|
|
49
70
|
if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
|
|
@@ -51,6 +72,7 @@ function eh() {
|
|
|
51
72
|
}
|
|
52
73
|
async function configureEntitleHub(opts) {
|
|
53
74
|
if (opts.iap) _iap = opts.iap;
|
|
75
|
+
else await loadIap();
|
|
54
76
|
client = new import_sdk.EntitleHub({
|
|
55
77
|
apiKey: opts.apiKey,
|
|
56
78
|
appUserId: opts.appUserId,
|
|
@@ -179,7 +201,7 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
179
201
|
}
|
|
180
202
|
fail(new PurchaseError(
|
|
181
203
|
"purchase-pending",
|
|
182
|
-
"The store hasn't confirmed this purchase yet. It may still complete
|
|
204
|
+
"The store hasn't confirmed this purchase yet. It may still complete, show a pending state, not a failure.",
|
|
183
205
|
lastInfo
|
|
184
206
|
));
|
|
185
207
|
};
|
|
@@ -212,12 +234,16 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
212
234
|
armTimer(sheetTimeoutMs);
|
|
213
235
|
});
|
|
214
236
|
}
|
|
215
|
-
async function restorePurchases() {
|
|
237
|
+
async function restorePurchases(options = {}) {
|
|
216
238
|
const I = iap();
|
|
217
239
|
const purchases = await I.getAvailablePurchases() || [];
|
|
218
240
|
for (const p of purchases) {
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
try {
|
|
242
|
+
await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (options.onReportError) options.onReportError(err, p);
|
|
245
|
+
else console.warn("[entitlehub] restore: could not report", p?.productId ?? p?.id ?? p?.sku, err);
|
|
246
|
+
}
|
|
221
247
|
}
|
|
222
248
|
return eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
223
249
|
}
|
|
@@ -247,6 +273,7 @@ async function reportToEntitleHub(purchase, productId, isSubscription) {
|
|
|
247
273
|
getOfferings,
|
|
248
274
|
getProducts,
|
|
249
275
|
isEntitled,
|
|
276
|
+
isReceiptRevoked,
|
|
250
277
|
logIn,
|
|
251
278
|
purchaseProduct,
|
|
252
279
|
restorePurchases
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CustomerInfo, Offerings } from '@entitlehub/sdk';
|
|
2
|
-
export { CustomerInfo, Offerings } from '@entitlehub/sdk';
|
|
2
|
+
export { CustomerInfo, Offerings, isReceiptRevoked } from '@entitlehub/sdk';
|
|
3
3
|
|
|
4
4
|
interface ConfigureOptions {
|
|
5
5
|
/** Your EntitleHub publishable key (pk_live_… / pk_test_…). Client-safe. */
|
|
@@ -13,9 +13,8 @@ interface ConfigureOptions {
|
|
|
13
13
|
* `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
|
|
14
14
|
* `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
|
|
15
15
|
*
|
|
16
|
-
* Pass your own
|
|
17
|
-
*
|
|
18
|
-
* a native issue in a particular store-library version without changing this SDK.
|
|
16
|
+
* Optional. Pass your own only to pin a specific version or to use a different OpenIAP-compatible
|
|
17
|
+
* library (e.g. `react-native-iap`); expo-iap is loaded for you otherwise.
|
|
19
18
|
*/
|
|
20
19
|
iap?: unknown;
|
|
21
20
|
}
|
|
@@ -31,13 +30,13 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
|
|
|
31
30
|
declare function getOfferings(): Promise<Offerings>;
|
|
32
31
|
/** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
|
|
33
32
|
declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
|
|
34
|
-
/** Why a purchase didn't complete. Check `code
|
|
33
|
+
/** Why a purchase didn't complete. Check `code`, never match on the message. */
|
|
35
34
|
type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
|
|
36
35
|
/**
|
|
37
36
|
* A purchase that didn't return entitlements.
|
|
38
37
|
*
|
|
39
38
|
* `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
|
|
39
|
+
* reached us yet (store notifications are asynchronous). Show "still confirming", not an error,
|
|
41
40
|
* the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
|
|
42
41
|
*/
|
|
43
42
|
declare class PurchaseError extends Error {
|
|
@@ -54,18 +53,18 @@ declare class PurchaseError extends Error {
|
|
|
54
53
|
*
|
|
55
54
|
* Store bridges disagree about how the result comes back, so we accept it from any source and never
|
|
56
55
|
* hang waiting for one of them:
|
|
57
|
-
* 1. `purchaseUpdatedListener
|
|
58
|
-
* 2. `requestPurchase`'s resolved value
|
|
56
|
+
* 1. `purchaseUpdatedListener`: expo-iap's event-based flow.
|
|
57
|
+
* 2. `requestPurchase`'s resolved value: react-native-iap 15+ / Nitro resolves with the purchase
|
|
59
58
|
* and may never emit the event.
|
|
60
|
-
* 3. Reconciliation after `timeoutMs
|
|
59
|
+
* 3. Reconciliation after `timeoutMs`: re-read `getAvailablePurchases()`, and failing that check
|
|
61
60
|
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
62
61
|
* grants it independently of the client).
|
|
63
62
|
*
|
|
64
63
|
* Rejects with a `PurchaseError`; branch on `.code`, never the message:
|
|
65
|
-
* - `purchase-cancelled
|
|
66
|
-
* - `purchase-pending`
|
|
64
|
+
* - `purchase-cancelled`: the user backed out.
|
|
65
|
+
* - `purchase-pending` : the store took the money but hasn't confirmed. NOT a failure; show a
|
|
67
66
|
* pending state. `.pending` is true and the entitlement will land on its own.
|
|
68
|
-
* - `purchase-failed`
|
|
67
|
+
* - `purchase-failed` : a real failure.
|
|
69
68
|
*/
|
|
70
69
|
declare function purchaseProduct(productId: string, opts?: {
|
|
71
70
|
isSubscription?: boolean;
|
|
@@ -77,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
|
|
|
77
76
|
/** How long to poll for confirmation before reporting pending (default 30s). */
|
|
78
77
|
reconcileMs?: number;
|
|
79
78
|
}): Promise<CustomerInfo>;
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
|
|
80
|
+
interface RestoreOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Called for each purchase that could not be reported. The restore continues either way, so one
|
|
83
|
+
* stale receipt can't cost the user the rest of their purchases.
|
|
84
|
+
*
|
|
85
|
+
* Worth handling: a restore is where a REFUND surfaces. Apple stamps a refunded transaction with
|
|
86
|
+
* revocationDate permanently and StoreKit keeps handing it back, so reporting it answers
|
|
87
|
+
* 409 `receipt_revoked` (EntitleHub revokes the grant server-side at the same time). That 409 is
|
|
88
|
+
* the only signal telling you the entitlement went away because the money was returned, rather
|
|
89
|
+
* than for some unexplained reason.
|
|
90
|
+
*/
|
|
91
|
+
onReportError?: (error: unknown, purchase: unknown) => void;
|
|
92
|
+
}
|
|
93
|
+
declare function restorePurchases(options?: RestoreOptions): Promise<CustomerInfo>;
|
|
82
94
|
/** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
|
|
83
95
|
declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
|
|
84
96
|
|
|
85
|
-
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
|
97
|
+
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, type RestoreOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CustomerInfo, Offerings } from '@entitlehub/sdk';
|
|
2
|
-
export { CustomerInfo, Offerings } from '@entitlehub/sdk';
|
|
2
|
+
export { CustomerInfo, Offerings, isReceiptRevoked } from '@entitlehub/sdk';
|
|
3
3
|
|
|
4
4
|
interface ConfigureOptions {
|
|
5
5
|
/** Your EntitleHub publishable key (pk_live_… / pk_test_…). Client-safe. */
|
|
@@ -13,9 +13,8 @@ interface ConfigureOptions {
|
|
|
13
13
|
* `fetchProducts`, `requestPurchase`, `purchaseUpdatedListener`, `purchaseErrorListener`,
|
|
14
14
|
* `finishTransaction`, `getAvailablePurchases`). Defaults to `expo-iap`.
|
|
15
15
|
*
|
|
16
|
-
* Pass your own
|
|
17
|
-
*
|
|
18
|
-
* a native issue in a particular store-library version without changing this SDK.
|
|
16
|
+
* Optional. Pass your own only to pin a specific version or to use a different OpenIAP-compatible
|
|
17
|
+
* library (e.g. `react-native-iap`); expo-iap is loaded for you otherwise.
|
|
19
18
|
*/
|
|
20
19
|
iap?: unknown;
|
|
21
20
|
}
|
|
@@ -31,13 +30,13 @@ declare function isEntitled(entitlementId: string): Promise<boolean>;
|
|
|
31
30
|
declare function getOfferings(): Promise<Offerings>;
|
|
32
31
|
/** Live store products (prices, localized titles) from the native store (expo-iap `fetchProducts`). */
|
|
33
32
|
declare function getProducts(productIds: string[], type?: "in-app" | "subs" | "all"): Promise<any[]>;
|
|
34
|
-
/** Why a purchase didn't complete. Check `code
|
|
33
|
+
/** Why a purchase didn't complete. Check `code`, never match on the message. */
|
|
35
34
|
type PurchaseErrorCode = "purchase-cancelled" | "purchase-pending" | "purchase-failed";
|
|
36
35
|
/**
|
|
37
36
|
* A purchase that didn't return entitlements.
|
|
38
37
|
*
|
|
39
38
|
* `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
|
|
39
|
+
* reached us yet (store notifications are asynchronous). Show "still confirming", not an error,
|
|
41
40
|
* the entitlement will arrive on its own, and `addCustomerInfoUpdateListener` will fire when it does.
|
|
42
41
|
*/
|
|
43
42
|
declare class PurchaseError extends Error {
|
|
@@ -54,18 +53,18 @@ declare class PurchaseError extends Error {
|
|
|
54
53
|
*
|
|
55
54
|
* Store bridges disagree about how the result comes back, so we accept it from any source and never
|
|
56
55
|
* hang waiting for one of them:
|
|
57
|
-
* 1. `purchaseUpdatedListener
|
|
58
|
-
* 2. `requestPurchase`'s resolved value
|
|
56
|
+
* 1. `purchaseUpdatedListener`: expo-iap's event-based flow.
|
|
57
|
+
* 2. `requestPurchase`'s resolved value: react-native-iap 15+ / Nitro resolves with the purchase
|
|
59
58
|
* and may never emit the event.
|
|
60
|
-
* 3. Reconciliation after `timeoutMs
|
|
59
|
+
* 3. Reconciliation after `timeoutMs`: re-read `getAvailablePurchases()`, and failing that check
|
|
61
60
|
* whether the entitlement appeared server-side anyway (EntitleHub's store-notification webhook
|
|
62
61
|
* grants it independently of the client).
|
|
63
62
|
*
|
|
64
63
|
* Rejects with a `PurchaseError`; branch on `.code`, never the message:
|
|
65
|
-
* - `purchase-cancelled
|
|
66
|
-
* - `purchase-pending`
|
|
64
|
+
* - `purchase-cancelled`: the user backed out.
|
|
65
|
+
* - `purchase-pending` : the store took the money but hasn't confirmed. NOT a failure; show a
|
|
67
66
|
* pending state. `.pending` is true and the entitlement will land on its own.
|
|
68
|
-
* - `purchase-failed`
|
|
67
|
+
* - `purchase-failed` : a real failure.
|
|
69
68
|
*/
|
|
70
69
|
declare function purchaseProduct(productId: string, opts?: {
|
|
71
70
|
isSubscription?: boolean;
|
|
@@ -77,9 +76,22 @@ declare function purchaseProduct(productId: string, opts?: {
|
|
|
77
76
|
/** How long to poll for confirmation before reporting pending (default 30s). */
|
|
78
77
|
reconcileMs?: number;
|
|
79
78
|
}): Promise<CustomerInfo>;
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
|
|
80
|
+
interface RestoreOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Called for each purchase that could not be reported. The restore continues either way, so one
|
|
83
|
+
* stale receipt can't cost the user the rest of their purchases.
|
|
84
|
+
*
|
|
85
|
+
* Worth handling: a restore is where a REFUND surfaces. Apple stamps a refunded transaction with
|
|
86
|
+
* revocationDate permanently and StoreKit keeps handing it back, so reporting it answers
|
|
87
|
+
* 409 `receipt_revoked` (EntitleHub revokes the grant server-side at the same time). That 409 is
|
|
88
|
+
* the only signal telling you the entitlement went away because the money was returned, rather
|
|
89
|
+
* than for some unexplained reason.
|
|
90
|
+
*/
|
|
91
|
+
onReportError?: (error: unknown, purchase: unknown) => void;
|
|
92
|
+
}
|
|
93
|
+
declare function restorePurchases(options?: RestoreOptions): Promise<CustomerInfo>;
|
|
82
94
|
/** Subscribe to entitlement changes (e.g. after a purchase). Returns an unsubscribe function. */
|
|
83
95
|
declare function addCustomerInfoUpdateListener(fn: (info: CustomerInfo) => void): () => void;
|
|
84
96
|
|
|
85
|
-
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
|
97
|
+
export { type ConfigureOptions, PurchaseError, type PurchaseErrorCode, type RestoreOptions, addCustomerInfoUpdateListener, configureEntitleHub, getCustomerInfo, getOfferings, getProducts, isEntitled, logIn, purchaseProduct, restorePurchases };
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/index.ts
|
|
9
2
|
import { EntitleHub } from "@entitlehub/sdk";
|
|
3
|
+
import { isReceiptRevoked } from "@entitlehub/sdk";
|
|
10
4
|
var _iap;
|
|
11
|
-
function
|
|
5
|
+
async function loadIap() {
|
|
12
6
|
if (_iap) return _iap;
|
|
7
|
+
let mod;
|
|
13
8
|
try {
|
|
14
|
-
|
|
9
|
+
mod = await import("expo-iap");
|
|
15
10
|
} catch {
|
|
16
11
|
throw new Error("@entitlehub/react-native needs 'expo-iap'. Install it with: npx expo install expo-iap");
|
|
17
12
|
}
|
|
13
|
+
_iap = typeof mod?.initConnection === "function" ? mod : mod?.default ?? mod;
|
|
14
|
+
if (typeof _iap?.initConnection !== "function") {
|
|
15
|
+
throw new Error("@entitlehub/react-native: the 'expo-iap' module did not load correctly (no initConnection). Check that expo-iap is installed and its version supports the OpenIAP API.");
|
|
16
|
+
}
|
|
17
|
+
return _iap;
|
|
18
|
+
}
|
|
19
|
+
function iap() {
|
|
20
|
+
if (!_iap) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub purchases.");
|
|
18
21
|
return _iap;
|
|
19
22
|
}
|
|
20
|
-
var RN_SDK_VERSION = "0.1.
|
|
23
|
+
var RN_SDK_VERSION = "0.1.8";
|
|
21
24
|
var client;
|
|
22
25
|
function eh() {
|
|
23
26
|
if (!client) throw new Error("Call configureEntitleHub({ apiKey, appUserId }) before using EntitleHub.");
|
|
@@ -25,6 +28,7 @@ function eh() {
|
|
|
25
28
|
}
|
|
26
29
|
async function configureEntitleHub(opts) {
|
|
27
30
|
if (opts.iap) _iap = opts.iap;
|
|
31
|
+
else await loadIap();
|
|
28
32
|
client = new EntitleHub({
|
|
29
33
|
apiKey: opts.apiKey,
|
|
30
34
|
appUserId: opts.appUserId,
|
|
@@ -153,7 +157,7 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
153
157
|
}
|
|
154
158
|
fail(new PurchaseError(
|
|
155
159
|
"purchase-pending",
|
|
156
|
-
"The store hasn't confirmed this purchase yet. It may still complete
|
|
160
|
+
"The store hasn't confirmed this purchase yet. It may still complete, show a pending state, not a failure.",
|
|
157
161
|
lastInfo
|
|
158
162
|
));
|
|
159
163
|
};
|
|
@@ -186,12 +190,16 @@ async function purchaseProduct(productId, opts = {}) {
|
|
|
186
190
|
armTimer(sheetTimeoutMs);
|
|
187
191
|
});
|
|
188
192
|
}
|
|
189
|
-
async function restorePurchases() {
|
|
193
|
+
async function restorePurchases(options = {}) {
|
|
190
194
|
const I = iap();
|
|
191
195
|
const purchases = await I.getAvailablePurchases() || [];
|
|
192
196
|
for (const p of purchases) {
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
try {
|
|
198
|
+
await reportToEntitleHub(p, p.productId ?? p.id ?? p.sku, true);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (options.onReportError) options.onReportError(err, p);
|
|
201
|
+
else console.warn("[entitlehub] restore: could not report", p?.productId ?? p?.id ?? p?.sku, err);
|
|
202
|
+
}
|
|
195
203
|
}
|
|
196
204
|
return eh().getCustomerInfo({ fetchPolicy: "network-only" });
|
|
197
205
|
}
|
|
@@ -220,6 +228,7 @@ export {
|
|
|
220
228
|
getOfferings,
|
|
221
229
|
getProducts,
|
|
222
230
|
isEntitled,
|
|
231
|
+
isReceiptRevoked,
|
|
223
232
|
logIn,
|
|
224
233
|
purchaseProduct,
|
|
225
234
|
restorePurchases
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entitlehub/react-native",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "EntitleHub for React Native & Expo
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
7
7
|
"react-native",
|
|
@@ -47,13 +47,14 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "npm run build && node --test \"test/*.test.mjs\"",
|
|
50
51
|
"prepublishOnly": "npm run build"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=18"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
|
-
"@entitlehub/sdk": ">=0.
|
|
57
|
+
"@entitlehub/sdk": ">=0.3.1",
|
|
57
58
|
"expo-iap": "*",
|
|
58
59
|
"react": ">=17"
|
|
59
60
|
},
|