react-native-iap 15.6.0 → 15.6.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.
Files changed (67) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +28 -4
  2. package/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt +13 -0
  3. package/ios/HybridRnIap.swift +25 -4
  4. package/ios/RnIapHelper.swift +3 -2
  5. package/ios/RnIapLog.swift +14 -0
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/index.js +72 -30
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/index.kepler.js +71 -5
  10. package/lib/module/index.kepler.js.map +1 -1
  11. package/lib/module/kit-api.js +78 -3
  12. package/lib/module/kit-api.js.map +1 -1
  13. package/lib/module/types.js +29 -25
  14. package/lib/module/types.js.map +1 -1
  15. package/lib/module/utils/deprecation.js +15 -0
  16. package/lib/module/utils/deprecation.js.map +1 -0
  17. package/lib/module/utils/platform-request.js +28 -0
  18. package/lib/module/utils/platform-request.js.map +1 -0
  19. package/lib/module/utils/type-bridge.js +9 -4
  20. package/lib/module/utils/type-bridge.js.map +1 -1
  21. package/lib/module/vega-adapter.js +31 -22
  22. package/lib/module/vega-adapter.js.map +1 -1
  23. package/lib/typescript/src/hooks/useIAP.d.ts +14 -2
  24. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +37 -12
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/lib/typescript/src/index.kepler.d.ts +49 -4
  28. package/lib/typescript/src/index.kepler.d.ts.map +1 -1
  29. package/lib/typescript/src/kit-api.d.ts +18 -8
  30. package/lib/typescript/src/kit-api.d.ts.map +1 -1
  31. package/lib/typescript/src/specs/RnIap.nitro.d.ts +24 -6
  32. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  33. package/lib/typescript/src/types.d.ts +105 -176
  34. package/lib/typescript/src/types.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/deprecation.d.ts +3 -0
  36. package/lib/typescript/src/utils/deprecation.d.ts.map +1 -0
  37. package/lib/typescript/src/utils/platform-request.d.ts +12 -0
  38. package/lib/typescript/src/utils/platform-request.d.ts.map +1 -0
  39. package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
  40. package/lib/typescript/src/vega-adapter.d.ts +1 -0
  41. package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
  42. package/nitrogen/generated/android/c++/JNitroPurchase.hpp +5 -1
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchase.kt +7 -2
  44. package/nitrogen/generated/ios/swift/NitroPurchase.swift +39 -2
  45. package/nitrogen/generated/shared/c++/NitroPurchase.hpp +5 -1
  46. package/openiap-versions.json +3 -3
  47. package/package.json +1 -1
  48. package/src/hooks/useIAP.ts +14 -2
  49. package/src/index.kepler.ts +82 -8
  50. package/src/index.ts +104 -47
  51. package/src/kit-api.ts +136 -13
  52. package/src/specs/RnIap.nitro.ts +24 -6
  53. package/src/types.ts +116 -204
  54. package/src/utils/deprecation.ts +16 -0
  55. package/src/utils/platform-request.ts +37 -0
  56. package/src/utils/type-bridge.ts +20 -4
  57. package/src/vega-adapter.ts +46 -24
  58. package/lib/module/hooks/useWebhookEvents.js +0 -113
  59. package/lib/module/hooks/useWebhookEvents.js.map +0 -1
  60. package/lib/module/webhook-client.js +0 -164
  61. package/lib/module/webhook-client.js.map +0 -1
  62. package/lib/typescript/src/hooks/useWebhookEvents.d.ts +0 -55
  63. package/lib/typescript/src/hooks/useWebhookEvents.d.ts.map +0 -1
  64. package/lib/typescript/src/webhook-client.d.ts +0 -82
  65. package/lib/typescript/src/webhook-client.d.ts.map +0 -1
  66. package/src/hooks/useWebhookEvents.ts +0 -180
  67. package/src/webhook-client.ts +0 -312
@@ -0,0 +1,16 @@
1
+ import {RnIapConsole} from './debug';
2
+
3
+ const emittedLegacyWarnings = new Set<string>();
4
+
5
+ export const warnLegacyOnce = (key: string, message: string): void => {
6
+ if (emittedLegacyWarnings.has(key)) {
7
+ return;
8
+ }
9
+
10
+ emittedLegacyWarnings.add(key);
11
+ RnIapConsole.warn(message);
12
+ };
13
+
14
+ export const resetLegacyWarningsForTesting = (): void => {
15
+ emittedLegacyWarnings.clear();
16
+ };
@@ -0,0 +1,37 @@
1
+ export interface PlatformRequestSelection<T> {
2
+ usesLegacyKey: boolean;
3
+ value: T | null | undefined;
4
+ }
5
+
6
+ /**
7
+ * Selects a canonical platform request by key presence, not value.
8
+ *
9
+ * An explicitly supplied canonical `null` or `undefined` is authoritative and
10
+ * must not fall back to the deprecated alias.
11
+ */
12
+ export const selectCanonicalPlatformRequest = <T>(
13
+ request: object,
14
+ canonicalKey: PropertyKey,
15
+ legacyKey: PropertyKey,
16
+ ): PlatformRequestSelection<T> => {
17
+ const record = request as Record<PropertyKey, T | null | undefined>;
18
+
19
+ if (Object.prototype.hasOwnProperty.call(record, canonicalKey)) {
20
+ return {
21
+ usesLegacyKey: false,
22
+ value: record[canonicalKey],
23
+ };
24
+ }
25
+
26
+ if (Object.prototype.hasOwnProperty.call(record, legacyKey)) {
27
+ return {
28
+ usesLegacyKey: true,
29
+ value: record[legacyKey],
30
+ };
31
+ }
32
+
33
+ return {
34
+ usesLegacyKey: false,
35
+ value: undefined,
36
+ };
37
+ };
@@ -470,8 +470,9 @@ export function convertNitroPurchaseToPurchase(
470
470
  isAutoRenewing: Boolean(nitroPurchase.isAutoRenewing),
471
471
  currentPlanId: toNullableString(nitroPurchase.currentPlanId),
472
472
  ids: nitroPurchase.ids ?? null,
473
- // PurchaseIOS requires both id and transactionId (they are the same value)
474
- transactionId: nitroPurchase.id,
473
+ // PurchaseIOS requires a transaction ID; legacy native payloads used id.
474
+ transactionId:
475
+ toNullableString(nitroPurchase.transactionId) ?? nitroPurchase.id,
475
476
  advancedCommerceInfoIOS: nitroPurchase.advancedCommerceInfoIOS ?? null,
476
477
  billingPlanTypeIOS: nitroPurchase.billingPlanTypeIOS ?? null,
477
478
  commitmentInfoIOS: nitroPurchase.commitmentInfoIOS ?? null,
@@ -525,6 +526,20 @@ export function convertNitroPurchaseToPurchase(
525
526
  return iosPurchase;
526
527
  }
527
528
 
529
+ const explicitAndroidTransactionId = toNullableString(
530
+ nitroPurchase.transactionId,
531
+ );
532
+ const legacyAndroidId = toNullableString(nitroPurchase.id);
533
+ const androidPurchaseToken = toNullableString(
534
+ nitroPurchase.purchaseToken ?? nitroPurchase.purchaseTokenAndroid,
535
+ );
536
+ const androidTransactionId =
537
+ explicitAndroidTransactionId ??
538
+ (legacyAndroidId != null &&
539
+ (store !== STORE_GOOGLE || legacyAndroidId !== androidPurchaseToken)
540
+ ? legacyAndroidId
541
+ : null);
542
+
528
543
  const androidPurchase: PurchaseAndroid = {
529
544
  id: nitroPurchase.id,
530
545
  productId: nitroPurchase.productId,
@@ -538,8 +553,9 @@ export function convertNitroPurchaseToPurchase(
538
553
  isAutoRenewing: Boolean(nitroPurchase.isAutoRenewing),
539
554
  currentPlanId: toNullableString(nitroPurchase.currentPlanId),
540
555
  ids: nitroPurchase.ids ?? null,
541
- // PurchaseAndroid has optional transactionId (may differ from id/orderId)
542
- transactionId: toNullableString(nitroPurchase.id),
556
+ // Android id falls back to purchaseToken when Play has no orderId, so do
557
+ // not synthesize a transactionId from it.
558
+ transactionId: androidTransactionId,
543
559
  autoRenewingAndroid: toNullableBoolean(
544
560
  nitroPurchase.autoRenewingAndroid ?? nitroPurchase.isAutoRenewing,
545
561
  ),
@@ -11,6 +11,7 @@ import type {
11
11
  } from './specs/RnIap.nitro';
12
12
  import {ErrorCode} from './types';
13
13
  import type {SubResponseCodeAndroid} from './types';
14
+ import {selectCanonicalPlatformRequest} from './utils/platform-request';
14
15
 
15
16
  type ResponseOperation =
16
17
  | 'product-data'
@@ -53,6 +54,7 @@ interface VegaProduct {
53
54
  interface VegaReceipt {
54
55
  cancelDate?: Date | number | string | null;
55
56
  deferredDate?: Date | number | string | null;
57
+ deferredSku?: string | null;
56
58
  isCancelled?: boolean | null;
57
59
  isDeferred?: boolean | null;
58
60
  productType?: unknown;
@@ -126,7 +128,6 @@ const FULFILLMENT_RESULT_FULFILLED = 1;
126
128
  const RESPONSE_SUCCESS = 1;
127
129
  const PURCHASE_RESPONSE_SUCCESS = 0;
128
130
  const PURCHASE_STATE_PURCHASED = 1;
129
- const PURCHASE_STATE_PENDING = 2;
130
131
  const IAPKIT_DEFAULT_BASE_URL = 'https://kit.openiap.dev';
131
132
  const IAPKIT_VERIFY_PATH = '/v1/purchase/verify';
132
133
  const VEGA_PARSER_ERROR_MESSAGES = [
@@ -574,8 +575,13 @@ function getSubscriptionPeriod(product: VegaProduct): string {
574
575
  return '';
575
576
  }
576
577
 
578
+ function nonBlankString(value: unknown): string | null {
579
+ if (typeof value !== 'string') return null;
580
+ return value.trim().length > 0 ? value : null;
581
+ }
582
+
577
583
  function getReceiptSku(receipt: VegaReceipt): string {
578
- return receipt.sku ?? receipt.termSku ?? '';
584
+ return nonBlankString(receipt.sku) ?? nonBlankString(receipt.termSku) ?? '';
579
585
  }
580
586
 
581
587
  function getCachedProductType(
@@ -711,38 +717,58 @@ function mapReceipt(
711
717
  const receiptId = receipt.receiptId ?? '';
712
718
  const productId = productIdOverride ?? getReceiptSku(receipt);
713
719
  const type = productTypeToOpenIap(receipt.productType ?? fallbackProductType);
714
- const isPending = Boolean(receipt.isDeferred);
715
720
  const isCanceled = Boolean(receipt.isCancelled || receipt.cancelDate);
716
- const isActive = !isCanceled && !isPending;
721
+ const isActive = !isCanceled;
722
+ const deferredSku = nonBlankString(receipt.deferredSku);
717
723
 
718
724
  return {
719
725
  id: receiptId,
726
+ transactionId: receiptId,
720
727
  productId,
721
728
  transactionDate: toTimestamp(receipt.purchaseDate),
722
729
  purchaseToken: receiptId,
723
- currentPlanId: type === 'subs' ? productId : null,
730
+ currentPlanId:
731
+ type === 'subs' ? (nonBlankString(receipt.termSku) ?? productId) : null,
724
732
  ids: productId ? [productId] : [],
725
733
  platform: 'android',
726
734
  store: 'amazon',
727
735
  quantity: 1,
728
- purchaseState: isPending ? 'pending' : isActive ? 'purchased' : 'unknown',
736
+ purchaseState: isActive ? 'purchased' : 'unknown',
729
737
  isAutoRenewing: type === 'subs' && isActive,
730
738
  purchaseTokenAndroid: receiptId,
731
739
  dataAndroid: stringifyJson(receipt),
732
740
  signatureAndroid: null,
733
741
  autoRenewingAndroid: type === 'subs' && isActive,
734
- purchaseStateAndroid: isPending
735
- ? PURCHASE_STATE_PENDING
736
- : isActive
737
- ? PURCHASE_STATE_PURCHASED
738
- : 0,
742
+ purchaseStateAndroid: isActive ? PURCHASE_STATE_PURCHASED : 0,
739
743
  isAcknowledgedAndroid: false,
740
- isSuspendedAndroid: Boolean(receipt.isDeferred),
744
+ packageNameAndroid: null,
745
+ obfuscatedAccountIdAndroid: null,
746
+ obfuscatedProfileIdAndroid: null,
747
+ developerPayloadAndroid: null,
748
+ isSuspendedAndroid: false,
749
+ pendingPurchaseUpdateAndroid:
750
+ receipt.isDeferred && deferredSku
751
+ ? {products: [deferredSku], purchaseToken: receiptId}
752
+ : null,
741
753
  };
742
754
  }
743
755
 
744
- function getSkuFromRequest(request: Parameters<RnIap['requestPurchase']>[0]) {
745
- const androidRequest = request.google ?? request.android;
756
+ type VegaPurchaseRequest = Parameters<RnIap['requestPurchase']>[0];
757
+ type VegaAndroidPurchaseRequest = NonNullable<VegaPurchaseRequest['google']>;
758
+
759
+ function selectGooglePurchaseRequest(
760
+ request: VegaPurchaseRequest,
761
+ ): VegaAndroidPurchaseRequest | null | undefined {
762
+ return selectCanonicalPlatformRequest<VegaAndroidPurchaseRequest>(
763
+ request,
764
+ 'google',
765
+ 'android',
766
+ ).value;
767
+ }
768
+
769
+ function getSkuFromRequest(
770
+ androidRequest: VegaAndroidPurchaseRequest | null | undefined,
771
+ ) {
746
772
  const skus = androidRequest?.skus ?? [];
747
773
  if (skus.length !== 1) {
748
774
  throw createVegaError(
@@ -1026,13 +1052,11 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap {
1026
1052
  options?: Parameters<RnIap['getAvailablePurchases']>[0],
1027
1053
  ): Promise<NitroPurchase[]> => {
1028
1054
  const requestedType = options?.android?.type;
1029
- const includeSuspended = Boolean(options?.android?.includeSuspended);
1030
1055
  const receipts = await getPurchaseUpdateReceipts();
1031
1056
  await hydrateProductTypesForReceipts(receipts);
1032
1057
  return receipts
1033
1058
  .filter((receipt) => {
1034
1059
  if (receipt.isCancelled || receipt.cancelDate) return false;
1035
- if (!includeSuspended && receipt.isDeferred) return false;
1036
1060
  const openIapType = productTypeToOpenIap(
1037
1061
  receipt.productType ??
1038
1062
  getCachedProductType(receipt, productTypesBySku),
@@ -1114,7 +1138,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap {
1114
1138
  const requestedPurchases: NitroPurchase[] = [];
1115
1139
 
1116
1140
  for (const receipt of receipts) {
1117
- if (receipt.isCancelled || receipt.cancelDate || receipt.isDeferred) {
1141
+ if (receipt.isCancelled || receipt.cancelDate) {
1118
1142
  continue;
1119
1143
  }
1120
1144
 
@@ -1422,8 +1446,8 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap {
1422
1446
  ): Promise<Awaited<ReturnType<RnIap['requestPurchase']>>> {
1423
1447
  let sku: string | undefined;
1424
1448
  try {
1425
- sku = getSkuFromRequest(request);
1426
- const androidRequest = request.google ?? request.android;
1449
+ const androidRequest = selectGooglePurchaseRequest(request);
1450
+ sku = getSkuFromRequest(androidRequest);
1427
1451
  const fallbackProductType = hasSubscriptionRequestContext(
1428
1452
  androidRequest?.subscriptionOffers,
1429
1453
  )
@@ -1515,8 +1539,8 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap {
1515
1539
  purchaseToken: purchase.purchaseToken ?? null,
1516
1540
  transactionDate: purchase.transactionDate,
1517
1541
  autoRenewingAndroid: purchase.autoRenewingAndroid ?? true,
1518
- basePlanIdAndroid: purchase.productId,
1519
- currentPlanId: purchase.productId,
1542
+ basePlanIdAndroid: purchase.currentPlanId ?? purchase.productId,
1543
+ currentPlanId: purchase.currentPlanId ?? purchase.productId,
1520
1544
  purchaseTokenAndroid: purchase.purchaseTokenAndroid ?? null,
1521
1545
  }));
1522
1546
  },
@@ -1540,9 +1564,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap {
1540
1564
  return true;
1541
1565
  },
1542
1566
  async restorePurchases(): Promise<void> {
1543
- const purchases = await getAvailablePurchases({
1544
- android: {includeSuspended: false},
1545
- });
1567
+ const purchases = await getAvailablePurchases();
1546
1568
  purchases.forEach(emitPurchaseUpdated);
1547
1569
  },
1548
1570
  addPurchaseUpdatedListener(listener): number {
@@ -1,113 +0,0 @@
1
- "use strict";
2
-
3
- import { useEffect, useRef, useState } from 'react';
4
- import { connectWebhookStream } from "../webhook-client.js";
5
- // React hook wrapping the SSE webhook stream. Lifecycle:
6
- // - opens on mount (once `apiKey` is non-empty),
7
- // - closes on unmount,
8
- // - reconnects automatically when EventSource raises a transport
9
- // error (the underlying client auto-reconnects via the EventSource
10
- // spec; this hook just surfaces the error and re-renders).
11
- //
12
- // Why a hook: openiap's UX guidance is that consumers consume webhook
13
- // events from React state (granting entitlement, refreshing the
14
- // subscription view) rather than via an imperative listener. The
15
- // hook's `events` buffer + `onEvent` callback cover both styles.
16
- export function useWebhookEvents({
17
- apiKey,
18
- baseUrl,
19
- eventSourceFactory,
20
- bufferSize = 50,
21
- onEvent,
22
- onError
23
- }) {
24
- const [events, setEvents] = useState([]);
25
- const [lastError, setLastError] = useState(null);
26
- const [isConnected, setIsConnected] = useState(false);
27
-
28
- // Stash callbacks in refs so reconnects don't fire on every render.
29
- // The underlying SSE connection should only restart when `apiKey` /
30
- // `baseUrl` change. `eventSourceFactory` is held in a ref too so
31
- // anonymous-function callers don't tear down the connection every
32
- // render (a common React pitfall — was previously documented as a
33
- // caller-side constraint, now enforced by the hook). `bufferSize`
34
- // is also a ref so adjusting the buffer cap from the host component
35
- // doesn't tear down the stream and lose in-flight events.
36
- const onEventRef = useRef(onEvent);
37
- const onErrorRef = useRef(onError);
38
- const eventSourceFactoryRef = useRef(eventSourceFactory);
39
- const bufferSizeRef = useRef(bufferSize);
40
- onEventRef.current = onEvent;
41
- onErrorRef.current = onError;
42
- eventSourceFactoryRef.current = eventSourceFactory;
43
- bufferSizeRef.current = bufferSize;
44
-
45
- // Trim the visible buffer immediately when bufferSize is lowered
46
- // mid-stream. The ref-based update would otherwise only take
47
- // effect on the next event.
48
- useEffect(() => {
49
- setEvents(prev => bufferSize > 0 ? prev.slice(0, bufferSize) : []);
50
- }, [bufferSize]);
51
- useEffect(() => {
52
- // Fresh stream → fresh state. Resetting events + lastError on
53
- // (re)connect prevents a stale payload from the previous
54
- // apiKey/baseUrl from briefly leaking into the new context.
55
- setEvents([]);
56
- setLastError(null);
57
- if (!apiKey) {
58
- return;
59
- }
60
- let listener = null;
61
- let mounted = true;
62
- try {
63
- listener = connectWebhookStream({
64
- apiKey,
65
- baseUrl,
66
- eventSourceFactory: eventSourceFactoryRef.current,
67
- onEvent: event => {
68
- if (!mounted) {
69
- return;
70
- }
71
- setIsConnected(true);
72
- const cap = bufferSizeRef.current;
73
- if (cap > 0) {
74
- setEvents(prev => [event, ...prev].slice(0, cap));
75
- }
76
- onEventRef.current?.(event);
77
- },
78
- onError: error => {
79
- if (!mounted) {
80
- return;
81
- }
82
- setLastError(error);
83
- onErrorRef.current?.(error);
84
- }
85
- });
86
- } catch (error) {
87
- const wrapped = {
88
- code: 'TRANSPORT_ERROR',
89
- message: error instanceof Error ? error.message : 'Failed to open webhook stream',
90
- cause: error
91
- };
92
- setLastError(wrapped);
93
- onErrorRef.current?.(wrapped);
94
- }
95
- return () => {
96
- mounted = false;
97
- listener?.close();
98
- setIsConnected(false);
99
- };
100
- // `eventSourceFactory` deliberately omitted from deps — held in a
101
- // ref above so anonymous-function callers don't trigger reconnects
102
- // on every render. The connection is only re-opened when apiKey or
103
- // baseUrl changes; a runtime factory swap is picked up on that
104
- // next reconnect via the ref.
105
- // eslint-disable-next-line react-hooks/exhaustive-deps
106
- }, [apiKey, baseUrl]);
107
- return {
108
- events,
109
- lastError,
110
- isConnected
111
- };
112
- }
113
- //# sourceMappingURL=useWebhookEvents.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["useEffect","useRef","useState","connectWebhookStream","useWebhookEvents","apiKey","baseUrl","eventSourceFactory","bufferSize","onEvent","onError","events","setEvents","lastError","setLastError","isConnected","setIsConnected","onEventRef","onErrorRef","eventSourceFactoryRef","bufferSizeRef","current","prev","slice","listener","mounted","event","cap","error","wrapped","code","message","Error","cause","close"],"sourceRoot":"../../../src","sources":["hooks/useWebhookEvents.ts"],"mappings":"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","ignoreList":[]}
@@ -1,164 +0,0 @@
1
- "use strict";
2
-
3
- // Transport-agnostic webhook client for the openiap kit SSE stream
4
- // (`GET /v1/webhooks/stream/{apiKey}`). Used by the JavaScript / TS
5
- // wrappers (react-native-iap, expo-iap) but written without React or
6
- // React-Native imports so it can also run in plain Node, browser, or
7
- // any other JS runtime.
8
- //
9
- // The wire format is documented in `packages/kit/server/api/v1/webhooks.ts`
10
- // and matches the GraphQL `WebhookEvent` shape from `webhook.graphql`.
11
- //
12
- // Parser logic is split out from the connection so it can be unit-
13
- // tested without a live server. See `webhook-client.test.ts`.
14
-
15
- export const WEBHOOK_EVENT_TYPES = ["SubscriptionStarted", "SubscriptionRenewed", "SubscriptionExpired", "SubscriptionInGracePeriod", "SubscriptionInBillingRetry", "SubscriptionRecovered", "SubscriptionCanceled", "SubscriptionUncanceled", "SubscriptionRevoked", "SubscriptionPriceChange", "SubscriptionProductChanged", "SubscriptionPaused", "SubscriptionResumed", "PurchaseRefunded", "PurchaseConsumptionRequest", "TestNotification"];
16
- const DEFAULT_BASE_URL = "https://kit.openiap.dev";
17
- export function connectWebhookStream(options) {
18
- const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
19
- const url = `${trimTrailingSlash(baseUrl)}/v1/webhooks/stream/${encodeURIComponent(options.apiKey)}`;
20
- const factory = options.eventSourceFactory ?? defaultEventSourceFactory;
21
- let stream;
22
- try {
23
- stream = factory(url, {});
24
- } catch (error) {
25
- options.onError?.({
26
- code: "NO_EVENTSOURCE",
27
- message: error instanceof Error ? error.message : "EventSource constructor unavailable in this runtime",
28
- cause: error
29
- });
30
- return {
31
- close: () => {}
32
- };
33
- }
34
- const seenIds = new Set();
35
- const seenOrder = [];
36
- const markSeen = id => {
37
- if (seenIds.has(id)) {
38
- return true;
39
- }
40
- seenIds.add(id);
41
- seenOrder.push(id);
42
- if (seenOrder.length > 1024) {
43
- const evicted = seenOrder.shift();
44
- if (evicted !== undefined) {
45
- seenIds.delete(evicted);
46
- }
47
- }
48
- return false;
49
- };
50
- const handleData = raw => {
51
- const parsed = parseWebhookEventData(raw);
52
- if (parsed.kind === "error") {
53
- options.onError?.({
54
- code: "PARSE_ERROR",
55
- message: parsed.message
56
- });
57
- return;
58
- }
59
- if (parsed.kind === "skip") {
60
- return;
61
- }
62
- if (markSeen(parsed.event.id)) {
63
- return;
64
- }
65
- options.onEvent(parsed.event);
66
- };
67
- if (typeof stream.addEventListener === "function") {
68
- stream.addEventListener("message", event => handleData(event.data));
69
- // WHATWG EventSource dispatches frames with `event: Foo` only to
70
- // listeners registered for `Foo`, not to `message` / `onmessage`.
71
- // Kit emits webhook frames as typed SSE events, so subscribe to
72
- // every known webhook type and keep `message` for older servers or
73
- // polyfills that collapse typed frames into the generic channel.
74
- for (const eventType of WEBHOOK_EVENT_TYPES) {
75
- stream.addEventListener(eventType, event => handleData(event.data));
76
- }
77
- } else {
78
- stream.onmessage = event => handleData(event.data);
79
- }
80
- stream.onerror = error => {
81
- options.onError?.({
82
- code: "TRANSPORT_ERROR",
83
- message: "SSE transport error (auto-reconnecting)",
84
- cause: error
85
- });
86
- };
87
- return {
88
- close: () => {
89
- try {
90
- stream.close();
91
- } catch {
92
- // Closing an already-closed EventSource is a no-op in browsers
93
- // but throws in some polyfills.
94
- }
95
- }
96
- };
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Pure helpers (exported for testing).
101
- // ---------------------------------------------------------------------------
102
-
103
- export function parseWebhookEventData(raw) {
104
- if (!raw) {
105
- return {
106
- kind: "skip",
107
- reason: "heartbeat"
108
- };
109
- }
110
- let parsed;
111
- try {
112
- parsed = JSON.parse(raw);
113
- } catch (error) {
114
- return {
115
- kind: "error",
116
- message: error instanceof Error ? `Failed to parse SSE payload: ${error.message}` : "Failed to parse SSE payload"
117
- };
118
- }
119
- if (typeof parsed !== "object" || parsed === null || !("type" in parsed) || typeof parsed.type !== "string") {
120
- // Stream-control messages (the `ready`/`stream-error` envelopes
121
- // emitted by the kit server) have no `type` and are surfaced as
122
- // skips so consumers don't see them as events.
123
- return {
124
- kind: "skip",
125
- reason: "stream-control"
126
- };
127
- }
128
- const event = parsed;
129
- if (typeof event.id !== "string" || typeof event.occurredAt !== "number" || typeof event.receivedAt !== "number") {
130
- return {
131
- kind: "error",
132
- message: `WebhookEvent missing required fields (id/occurredAt/receivedAt)`
133
- };
134
- }
135
- // purchaseToken is required for every event type *except*
136
- // TestNotification — Apple ASN v2 / Google RTDN test payloads
137
- // carry no transaction. Hard-rejecting here would surface valid
138
- // test webhooks as MALFORMED_EVENT and never reach listeners.
139
- if (event.type !== "TestNotification" && typeof event.purchaseToken !== "string") {
140
- return {
141
- kind: "error",
142
- message: `WebhookEvent missing required field purchaseToken`
143
- };
144
- }
145
- return {
146
- kind: "ok",
147
- event
148
- };
149
- }
150
- function trimTrailingSlash(url) {
151
- return url.endsWith("/") ? url.slice(0, -1) : url;
152
- }
153
- function defaultEventSourceFactory(url, _headers) {
154
- // EventSource is part of the WHATWG spec and available in all
155
- // browser environments and most JS runtimes (Bun, Node 22+, Deno).
156
- // RN does not ship it natively — consumers must pass
157
- // `eventSourceFactory` from `react-native-sse` or similar.
158
- const ctor = globalThis.EventSource;
159
- if (!ctor) {
160
- throw new Error("EventSource is not defined. Pass `eventSourceFactory` for runtimes without a built-in EventSource.");
161
- }
162
- return new ctor(url);
163
- }
164
- //# sourceMappingURL=webhook-client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["WEBHOOK_EVENT_TYPES","DEFAULT_BASE_URL","connectWebhookStream","options","baseUrl","url","trimTrailingSlash","encodeURIComponent","apiKey","factory","eventSourceFactory","defaultEventSourceFactory","stream","error","onError","code","message","Error","cause","close","seenIds","Set","seenOrder","markSeen","id","has","add","push","length","evicted","shift","undefined","delete","handleData","raw","parsed","parseWebhookEventData","kind","event","onEvent","addEventListener","data","eventType","onmessage","onerror","reason","JSON","parse","type","occurredAt","receivedAt","purchaseToken","endsWith","slice","_headers","ctor","globalThis","EventSource"],"sourceRoot":"../../src","sources":["webhook-client.ts"],"mappings":"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","ignoreList":[]}
@@ -1,55 +0,0 @@
1
- import { type WebhookEventPayload, type WebhookEventStream, type WebhookListenerError } from '../webhook-client';
2
- export type UseWebhookEventsOptions = {
3
- /**
4
- * kit project API key — same value used for receipt verification.
5
- * Must be non-empty to start the stream; pass `null`/`undefined` to
6
- * disable the listener (e.g. before the user is logged in).
7
- */
8
- apiKey: string | null | undefined;
9
- /**
10
- * Override the kit base URL. Defaults to https://kit.openiap.dev.
11
- */
12
- baseUrl?: string;
13
- /**
14
- * Optional EventSource factory. Required on React Native because RN
15
- * does not ship a global EventSource — pass an instance from
16
- * `react-native-sse` (or any compatible polyfill).
17
- */
18
- eventSourceFactory?: (url: string, headers: Record<string, string>) => WebhookEventStream;
19
- /**
20
- * Maximum number of events to retain in the in-memory ring buffer
21
- * surfaced as `events`. Older entries are discarded. Defaults to 50.
22
- * Set 0 to opt out of the buffer entirely (consume only via
23
- * `onEvent`).
24
- */
25
- bufferSize?: number;
26
- /**
27
- * Called for every received event in addition to being appended to
28
- * the buffer. Useful for side effects (toast, analytics, granting
29
- * entitlement). Called with the latest stable callback identity.
30
- */
31
- onEvent?: (event: WebhookEventPayload) => void;
32
- /**
33
- * Called when the stream surfaces a transport / parse error.
34
- * EventSource auto-reconnects regardless of this hook — this is
35
- * primarily for telemetry + UI surfacing.
36
- */
37
- onError?: (error: WebhookListenerError) => void;
38
- };
39
- export type UseWebhookEventsResult = {
40
- /** Most recent N events (most-recent-first). Capped at bufferSize. */
41
- events: WebhookEventPayload[];
42
- /** Last error reported by the underlying stream. Null when healthy. */
43
- lastError: WebhookListenerError | null;
44
- /**
45
- * True once the first webhook event has been received from the
46
- * stream. Remains false if the connection is open but idle (the
47
- * underlying SSE bridge doesn't surface a "stream opened"
48
- * lifecycle event we can hook into; isConnected is therefore an
49
- * activity indicator, not a raw socket-state flag). Reset to
50
- * false on cleanup / apiKey change.
51
- */
52
- isConnected: boolean;
53
- };
54
- export declare function useWebhookEvents({ apiKey, baseUrl, eventSourceFactory, bufferSize, onEvent, onError, }: UseWebhookEventsOptions): UseWebhookEventsResult;
55
- //# sourceMappingURL=useWebhookEvents.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useWebhookEvents.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useWebhookEvents.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EAEvB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC5B,kBAAkB,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC/C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,sEAAsE;IACtE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,uEAAuE;IACvE,SAAS,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACvC;;;;;;;OAOG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAaF,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,OAAO,EACP,kBAAkB,EAClB,UAAe,EACf,OAAO,EACP,OAAO,GACR,EAAE,uBAAuB,GAAG,sBAAsB,CA8FlD"}