react-native-iap 15.2.0 → 15.2.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/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +117 -114
- package/android/src/main/java/com/margelo/nitro/iap/ProductQueryHelpers.kt +42 -0
- package/android/src/test/java/com/margelo/nitro/iap/ProductQueryHelpersTest.kt +140 -0
- package/ios/HybridRnIap.swift +33 -0
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/hooks/useWebhookEvents.js +113 -0
- package/lib/module/hooks/useWebhookEvents.js.map +1 -0
- package/lib/module/index.js +331 -131
- package/lib/module/index.js.map +1 -1
- package/lib/module/kit-api.js +161 -0
- package/lib/module/kit-api.js.map +1 -0
- package/lib/module/types.js +16 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/utils/errorMapping.js +6 -0
- package/lib/module/utils/errorMapping.js.map +1 -1
- package/lib/module/webhook-client.js +164 -0
- package/lib/module/webhook-client.js.map +1 -0
- package/lib/typescript/plugin/src/withIAP.d.ts +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +162 -2
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useWebhookEvents.d.ts +55 -0
- package/lib/typescript/src/hooks/useWebhookEvents.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +282 -129
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/kit-api.d.ts +54 -0
- package/lib/typescript/src/kit-api.d.ts.map +1 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +7 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +304 -74
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts +3 -0
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts +6 -0
- package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
- package/lib/typescript/src/webhook-client.d.ts +82 -0
- package/lib/typescript/src/webhook-client.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +3 -0
- package/nitrogen/generated/android/c++/JAdvancedCommerceInfoIOS.hpp +118 -0
- package/nitrogen/generated/android/c++/JAdvancedCommerceItemDetailsIOS.hpp +62 -0
- package/nitrogen/generated/android/c++/JAdvancedCommerceItemIOS.hpp +78 -0
- package/nitrogen/generated/android/c++/JAdvancedCommerceRefundIOS.hpp +62 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +44 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JPurchase.hpp +11 -0
- package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +16 -1
- package/nitrogen/generated/android/c++/JRequestPurchaseResult.hpp +11 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.hpp +84 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.cpp +35 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.hpp +84 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceInfoIOS.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemDetailsIOS.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemIOS.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceRefundIOS.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceInfoIOS.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceItemDetailsIOS.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_Array_AdvancedCommerceRefundIOS_.kt +53 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +166 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +12 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +20 -0
- package/nitrogen/generated/ios/swift/AdvancedCommerceInfoIOS.swift +294 -0
- package/nitrogen/generated/ios/swift/AdvancedCommerceItemDetailsIOS.swift +61 -0
- package/nitrogen/generated/ios/swift/AdvancedCommerceItemIOS.swift +141 -0
- package/nitrogen/generated/ios/swift/AdvancedCommerceRefundIOS.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +25 -0
- package/nitrogen/generated/ios/swift/PurchaseIOS.swift +39 -2
- package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceInfoIOS.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceItemDetailsIOS.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType__AdvancedCommerceRefundIOS_.swift +18 -0
- package/nitrogen/generated/shared/c++/AdvancedCommerceInfoIOS.hpp +117 -0
- package/nitrogen/generated/shared/c++/AdvancedCommerceItemDetailsIOS.hpp +86 -0
- package/nitrogen/generated/shared/c++/AdvancedCommerceItemIOS.hpp +99 -0
- package/nitrogen/generated/shared/c++/AdvancedCommerceRefundIOS.hpp +86 -0
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +9 -2
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/plugin/build/withIAP.d.ts +1 -1
- package/plugin/src/withIAP.ts +1 -1
- package/src/hooks/useIAP.ts +162 -2
- package/src/hooks/useWebhookEvents.ts +180 -0
- package/src/index.ts +348 -130
- package/src/kit-api.ts +225 -0
- package/src/specs/RnIap.nitro.ts +8 -0
- package/src/types.ts +314 -74
- package/src/utils/error.ts +3 -0
- package/src/utils/errorMapping.ts +12 -0
- package/src/webhook-client.ts +312 -0
package/src/hooks/useIAP.ts
CHANGED
|
@@ -64,33 +64,178 @@ type UseIap = {
|
|
|
64
64
|
availablePurchases: Purchase[];
|
|
65
65
|
promotedProductIOS?: Product;
|
|
66
66
|
activeSubscriptions: ActiveSubscription[];
|
|
67
|
+
/**
|
|
68
|
+
* Complete a purchase transaction. Call after server-side verification to remove it
|
|
69
|
+
* from the queue.
|
|
70
|
+
*
|
|
71
|
+
* @param args.purchase The `Purchase` to finalize.
|
|
72
|
+
* @param args.isConsumable `true` for consumables (consumes the token so the SKU can be
|
|
73
|
+
* re-bought, e.g. coins); `false` (default) for non-consumables and subscriptions.
|
|
74
|
+
* @returns Promise that resolves once the platform finalizes the transaction.
|
|
75
|
+
* @throws When the platform finalize call fails.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* purchaseUpdatedListener(async (purchase) => {
|
|
80
|
+
* if (await verifyOnServer(purchase)) {
|
|
81
|
+
* await finishTransaction({ purchase, isConsumable: false });
|
|
82
|
+
* }
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @remarks **Critical:** Android purchases must be finalized within 3 days or Google
|
|
87
|
+
* auto-refunds. iOS unfinished transactions replay on every app launch.
|
|
88
|
+
*
|
|
89
|
+
* @see {@link https://www.openiap.dev/docs/apis/finish-transaction}
|
|
90
|
+
*/
|
|
67
91
|
finishTransaction: (args: MutationFinishTransactionArgs) => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* List the user's unfinished purchases — non-consumables, active subscriptions, and any
|
|
94
|
+
* pending transactions not yet finished.
|
|
95
|
+
*
|
|
96
|
+
* @param options Optional `PurchaseOptions`.
|
|
97
|
+
* - iOS: `alsoPublishToEventListenerIOS`, `onlyIncludeActiveItemsIOS`.
|
|
98
|
+
* - Android: `includeSuspendedAndroid` (include subscriptions in a paused/grace state).
|
|
99
|
+
* @returns Promise that resolves when the request is dispatched; results land in the
|
|
100
|
+
* hook's reactive `availablePurchases` state — read from there, don't expect a return value.
|
|
101
|
+
* @throws When the platform query fails.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const { availablePurchases, getAvailablePurchases, finishTransaction } = useIAP();
|
|
106
|
+
*
|
|
107
|
+
* useEffect(() => {
|
|
108
|
+
* void getAvailablePurchases();
|
|
109
|
+
* }, [getAvailablePurchases]);
|
|
110
|
+
*
|
|
111
|
+
* useEffect(() => {
|
|
112
|
+
* for (const p of availablePurchases) {
|
|
113
|
+
* void verifyOnServer(p).then((ok) => {
|
|
114
|
+
* if (ok) finishTransaction({ purchase: p, isConsumable: false });
|
|
115
|
+
* });
|
|
116
|
+
* }
|
|
117
|
+
* }, [availablePurchases, finishTransaction]);
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @see {@link https://www.openiap.dev/docs/apis/get-available-purchases}
|
|
121
|
+
*/
|
|
68
122
|
getAvailablePurchases: (options?: PurchaseOptions) => Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Retrieve products or subscriptions from the store by SKU.
|
|
125
|
+
*
|
|
126
|
+
* @param params `ProductRequest` — `skus` (string[]) and optional `type`
|
|
127
|
+
* (`'in-app' | 'subs' | 'all'`, defaults to `'in-app'`).
|
|
128
|
+
* @returns Promise that resolves when the request is dispatched; results land in the
|
|
129
|
+
* hook's reactive `products` / `subscriptions` state — read from there, don't expect a return value.
|
|
130
|
+
* @throws When the store rejects the request (empty `skus`, not connected,
|
|
131
|
+
* network/store error). Unknown SKUs are simply omitted from the result, not thrown.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const { products, fetchProducts } = useIAP();
|
|
136
|
+
*
|
|
137
|
+
* useEffect(() => {
|
|
138
|
+
* void fetchProducts({
|
|
139
|
+
* skus: ['com.app.coins_100', 'com.app.premium'],
|
|
140
|
+
* type: 'in-app',
|
|
141
|
+
* });
|
|
142
|
+
* }, [fetchProducts]);
|
|
143
|
+
*
|
|
144
|
+
* // Render `products` directly from hook state.
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @remarks This is a regular promise-based call. Don't confuse with `request*` APIs
|
|
148
|
+
* (`requestPurchase`), which are event-based.
|
|
149
|
+
*
|
|
150
|
+
* @see {@link https://www.openiap.dev/docs/apis/fetch-products}
|
|
151
|
+
*/
|
|
69
152
|
fetchProducts: (params: {
|
|
70
153
|
skus: string[];
|
|
71
154
|
type?: ProductQueryType | null;
|
|
72
155
|
}) => Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Initiate a purchase or subscription flow. The result is delivered through
|
|
158
|
+
* `purchaseUpdatedListener` — NOT the return value.
|
|
159
|
+
*
|
|
160
|
+
* @param props `RequestPurchaseProps`, discriminated by `type`:
|
|
161
|
+
* - `type: 'in-app'` — pass `request.apple.sku` (iOS) and/or `request.google.skus` (Android).
|
|
162
|
+
* - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
|
|
163
|
+
* @returns Promise that resolves when the request is dispatched; results land in the
|
|
164
|
+
* hook's `onPurchaseSuccess` / `onPurchaseError` callbacks.
|
|
165
|
+
* @throws Synchronous rejection from the store (e.g. `E_NOT_PREPARED`, validation failure).
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* await requestPurchase({
|
|
170
|
+
* request: {
|
|
171
|
+
* apple: { sku: 'com.app.premium' },
|
|
172
|
+
* google: { skus: ['com.app.premium'] },
|
|
173
|
+
* },
|
|
174
|
+
* type: 'in-app',
|
|
175
|
+
* });
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @remarks Event-based. Listen for the result via {@link purchaseUpdatedListener} /
|
|
179
|
+
* {@link purchaseErrorListener}, or use `useIAP({ onPurchaseSuccess, onPurchaseError })`.
|
|
180
|
+
*
|
|
181
|
+
* @see {@link https://www.openiap.dev/docs/apis/request-purchase}
|
|
182
|
+
*/
|
|
73
183
|
requestPurchase: (params: RequestPurchaseProps) => Promise<void>;
|
|
74
184
|
/**
|
|
75
185
|
* @deprecated Use `verifyPurchase` instead. This function will be removed in a future version.
|
|
186
|
+
*
|
|
187
|
+
* @see {@link https://www.openiap.dev/docs/apis/validate-receipt}
|
|
76
188
|
*/
|
|
77
189
|
validateReceipt: (
|
|
78
190
|
options: VerifyPurchaseProps,
|
|
79
191
|
) => Promise<VerifyPurchaseResult>;
|
|
80
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Verify a purchase against your own backend (returns isValid + raw store metadata).
|
|
194
|
+
*
|
|
195
|
+
* @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase}
|
|
196
|
+
*/
|
|
81
197
|
verifyPurchase: (
|
|
82
198
|
options: VerifyPurchaseProps,
|
|
83
199
|
) => Promise<VerifyPurchaseResult>;
|
|
84
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Verify via a managed provider — currently only `iapkit` (IAPKit). The PurchaseVerificationProvider enum exposes no other provider literal today.
|
|
202
|
+
*
|
|
203
|
+
* @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider}
|
|
204
|
+
*/
|
|
85
205
|
verifyPurchaseWithProvider: (
|
|
86
206
|
options: VerifyPurchaseWithProviderProps,
|
|
87
207
|
) => Promise<VerifyPurchaseWithProviderResult>;
|
|
208
|
+
/**
|
|
209
|
+
* Restore non-consumable and active subscription purchases.
|
|
210
|
+
*
|
|
211
|
+
* @see {@link https://www.openiap.dev/docs/apis/restore-purchases}
|
|
212
|
+
*/
|
|
88
213
|
restorePurchases: (options?: PurchaseOptions) => Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* Read the App Store-promoted product, if any.
|
|
216
|
+
*
|
|
217
|
+
* @see {@link https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios}
|
|
218
|
+
*/
|
|
89
219
|
getPromotedProductIOS: () => Promise<Product | null>;
|
|
220
|
+
/**
|
|
221
|
+
* Buy the currently promoted product.
|
|
222
|
+
*
|
|
223
|
+
* @see {@link https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios}
|
|
224
|
+
*/
|
|
90
225
|
requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
|
|
226
|
+
/**
|
|
227
|
+
* Get details of all currently active subscriptions.
|
|
228
|
+
*
|
|
229
|
+
* @see {@link https://www.openiap.dev/docs/apis/get-active-subscriptions}
|
|
230
|
+
*/
|
|
91
231
|
getActiveSubscriptions: (
|
|
92
232
|
subscriptionIds?: string[],
|
|
93
233
|
) => Promise<ActiveSubscription[]>;
|
|
234
|
+
/**
|
|
235
|
+
* Check whether the user has any active subscription.
|
|
236
|
+
*
|
|
237
|
+
* @see {@link https://www.openiap.dev/docs/apis/has-active-subscriptions}
|
|
238
|
+
*/
|
|
94
239
|
hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
|
|
95
240
|
/**
|
|
96
241
|
* Manually retry the store connection.
|
|
@@ -99,8 +244,23 @@ type UseIap = {
|
|
|
99
244
|
*/
|
|
100
245
|
reconnect: () => Promise<boolean>;
|
|
101
246
|
// Alternative billing (Android)
|
|
247
|
+
/**
|
|
248
|
+
* Check whether alternative billing is available for the user.
|
|
249
|
+
*
|
|
250
|
+
* @see {@link https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android}
|
|
251
|
+
*/
|
|
102
252
|
checkAlternativeBillingAvailabilityAndroid?: () => Promise<boolean>;
|
|
253
|
+
/**
|
|
254
|
+
* Display Google's alternative billing information dialog.
|
|
255
|
+
*
|
|
256
|
+
* @see {@link https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android}
|
|
257
|
+
*/
|
|
103
258
|
showAlternativeBillingDialogAndroid?: () => Promise<boolean>;
|
|
259
|
+
/**
|
|
260
|
+
* Create a reporting token for an alternative billing flow.
|
|
261
|
+
*
|
|
262
|
+
* @see {@link https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android}
|
|
263
|
+
*/
|
|
104
264
|
createAlternativeBillingTokenAndroid?: (
|
|
105
265
|
sku?: string,
|
|
106
266
|
) => Promise<string | null>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
connectWebhookStream,
|
|
5
|
+
type WebhookEventPayload,
|
|
6
|
+
type WebhookEventStream,
|
|
7
|
+
type WebhookListener,
|
|
8
|
+
type WebhookListenerError,
|
|
9
|
+
} from '../webhook-client';
|
|
10
|
+
|
|
11
|
+
export type UseWebhookEventsOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* kit project API key — same value used for receipt verification.
|
|
14
|
+
* Must be non-empty to start the stream; pass `null`/`undefined` to
|
|
15
|
+
* disable the listener (e.g. before the user is logged in).
|
|
16
|
+
*/
|
|
17
|
+
apiKey: string | null | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Override the kit base URL. Defaults to https://kit.openiap.dev.
|
|
20
|
+
*/
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional EventSource factory. Required on React Native because RN
|
|
24
|
+
* does not ship a global EventSource — pass an instance from
|
|
25
|
+
* `react-native-sse` (or any compatible polyfill).
|
|
26
|
+
*/
|
|
27
|
+
eventSourceFactory?: (
|
|
28
|
+
url: string,
|
|
29
|
+
headers: Record<string, string>,
|
|
30
|
+
) => WebhookEventStream;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum number of events to retain in the in-memory ring buffer
|
|
33
|
+
* surfaced as `events`. Older entries are discarded. Defaults to 50.
|
|
34
|
+
* Set 0 to opt out of the buffer entirely (consume only via
|
|
35
|
+
* `onEvent`).
|
|
36
|
+
*/
|
|
37
|
+
bufferSize?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Called for every received event in addition to being appended to
|
|
40
|
+
* the buffer. Useful for side effects (toast, analytics, granting
|
|
41
|
+
* entitlement). Called with the latest stable callback identity.
|
|
42
|
+
*/
|
|
43
|
+
onEvent?: (event: WebhookEventPayload) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called when the stream surfaces a transport / parse error.
|
|
46
|
+
* EventSource auto-reconnects regardless of this hook — this is
|
|
47
|
+
* primarily for telemetry + UI surfacing.
|
|
48
|
+
*/
|
|
49
|
+
onError?: (error: WebhookListenerError) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type UseWebhookEventsResult = {
|
|
53
|
+
/** Most recent N events (most-recent-first). Capped at bufferSize. */
|
|
54
|
+
events: WebhookEventPayload[];
|
|
55
|
+
/** Last error reported by the underlying stream. Null when healthy. */
|
|
56
|
+
lastError: WebhookListenerError | null;
|
|
57
|
+
/**
|
|
58
|
+
* True once the first webhook event has been received from the
|
|
59
|
+
* stream. Remains false if the connection is open but idle (the
|
|
60
|
+
* underlying SSE bridge doesn't surface a "stream opened"
|
|
61
|
+
* lifecycle event we can hook into; isConnected is therefore an
|
|
62
|
+
* activity indicator, not a raw socket-state flag). Reset to
|
|
63
|
+
* false on cleanup / apiKey change.
|
|
64
|
+
*/
|
|
65
|
+
isConnected: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// React hook wrapping the SSE webhook stream. Lifecycle:
|
|
69
|
+
// - opens on mount (once `apiKey` is non-empty),
|
|
70
|
+
// - closes on unmount,
|
|
71
|
+
// - reconnects automatically when EventSource raises a transport
|
|
72
|
+
// error (the underlying client auto-reconnects via the EventSource
|
|
73
|
+
// spec; this hook just surfaces the error and re-renders).
|
|
74
|
+
//
|
|
75
|
+
// Why a hook: openiap's UX guidance is that consumers consume webhook
|
|
76
|
+
// events from React state (granting entitlement, refreshing the
|
|
77
|
+
// subscription view) rather than via an imperative listener. The
|
|
78
|
+
// hook's `events` buffer + `onEvent` callback cover both styles.
|
|
79
|
+
export function useWebhookEvents({
|
|
80
|
+
apiKey,
|
|
81
|
+
baseUrl,
|
|
82
|
+
eventSourceFactory,
|
|
83
|
+
bufferSize = 50,
|
|
84
|
+
onEvent,
|
|
85
|
+
onError,
|
|
86
|
+
}: UseWebhookEventsOptions): UseWebhookEventsResult {
|
|
87
|
+
const [events, setEvents] = useState<WebhookEventPayload[]>([]);
|
|
88
|
+
const [lastError, setLastError] = useState<WebhookListenerError | null>(null);
|
|
89
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
90
|
+
|
|
91
|
+
// Stash callbacks in refs so reconnects don't fire on every render.
|
|
92
|
+
// The underlying SSE connection should only restart when `apiKey` /
|
|
93
|
+
// `baseUrl` change. `eventSourceFactory` is held in a ref too so
|
|
94
|
+
// anonymous-function callers don't tear down the connection every
|
|
95
|
+
// render (a common React pitfall — was previously documented as a
|
|
96
|
+
// caller-side constraint, now enforced by the hook). `bufferSize`
|
|
97
|
+
// is also a ref so adjusting the buffer cap from the host component
|
|
98
|
+
// doesn't tear down the stream and lose in-flight events.
|
|
99
|
+
const onEventRef = useRef(onEvent);
|
|
100
|
+
const onErrorRef = useRef(onError);
|
|
101
|
+
const eventSourceFactoryRef = useRef(eventSourceFactory);
|
|
102
|
+
const bufferSizeRef = useRef(bufferSize);
|
|
103
|
+
onEventRef.current = onEvent;
|
|
104
|
+
onErrorRef.current = onError;
|
|
105
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
106
|
+
bufferSizeRef.current = bufferSize;
|
|
107
|
+
|
|
108
|
+
// Trim the visible buffer immediately when bufferSize is lowered
|
|
109
|
+
// mid-stream. The ref-based update would otherwise only take
|
|
110
|
+
// effect on the next event.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
setEvents((prev) => (bufferSize > 0 ? prev.slice(0, bufferSize) : []));
|
|
113
|
+
}, [bufferSize]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
// Fresh stream → fresh state. Resetting events + lastError on
|
|
117
|
+
// (re)connect prevents a stale payload from the previous
|
|
118
|
+
// apiKey/baseUrl from briefly leaking into the new context.
|
|
119
|
+
setEvents([]);
|
|
120
|
+
setLastError(null);
|
|
121
|
+
|
|
122
|
+
if (!apiKey) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let listener: WebhookListener | null = null;
|
|
127
|
+
let mounted = true;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
listener = connectWebhookStream({
|
|
131
|
+
apiKey,
|
|
132
|
+
baseUrl,
|
|
133
|
+
eventSourceFactory: eventSourceFactoryRef.current,
|
|
134
|
+
onEvent: (event) => {
|
|
135
|
+
if (!mounted) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
setIsConnected(true);
|
|
139
|
+
const cap = bufferSizeRef.current;
|
|
140
|
+
if (cap > 0) {
|
|
141
|
+
setEvents((prev) => [event, ...prev].slice(0, cap));
|
|
142
|
+
}
|
|
143
|
+
onEventRef.current?.(event);
|
|
144
|
+
},
|
|
145
|
+
onError: (error) => {
|
|
146
|
+
if (!mounted) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
setLastError(error);
|
|
150
|
+
onErrorRef.current?.(error);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const wrapped: WebhookListenerError = {
|
|
155
|
+
code: 'TRANSPORT_ERROR',
|
|
156
|
+
message:
|
|
157
|
+
error instanceof Error
|
|
158
|
+
? error.message
|
|
159
|
+
: 'Failed to open webhook stream',
|
|
160
|
+
cause: error,
|
|
161
|
+
};
|
|
162
|
+
setLastError(wrapped);
|
|
163
|
+
onErrorRef.current?.(wrapped);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
mounted = false;
|
|
168
|
+
listener?.close();
|
|
169
|
+
setIsConnected(false);
|
|
170
|
+
};
|
|
171
|
+
// `eventSourceFactory` deliberately omitted from deps — held in a
|
|
172
|
+
// ref above so anonymous-function callers don't trigger reconnects
|
|
173
|
+
// on every render. The connection is only re-opened when apiKey or
|
|
174
|
+
// baseUrl changes; a runtime factory swap is picked up on that
|
|
175
|
+
// next reconnect via the ref.
|
|
176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
|
+
}, [apiKey, baseUrl]);
|
|
178
|
+
|
|
179
|
+
return {events, lastError, isConnected};
|
|
180
|
+
}
|