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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Transport-agnostic webhook client for the openiap kit SSE stream
|
|
2
|
+
// (`GET /v1/webhooks/stream/{apiKey}`). Used by the JavaScript / TS
|
|
3
|
+
// wrappers (react-native-iap, expo-iap) but written without React or
|
|
4
|
+
// React-Native imports so it can also run in plain Node, browser, or
|
|
5
|
+
// any other JS runtime.
|
|
6
|
+
//
|
|
7
|
+
// The wire format is documented in `packages/kit/server/api/v1/webhooks.ts`
|
|
8
|
+
// and matches the GraphQL `WebhookEvent` shape from `webhook.graphql`.
|
|
9
|
+
//
|
|
10
|
+
// Parser logic is split out from the connection so it can be unit-
|
|
11
|
+
// tested without a live server. See `webhook-client.test.ts`.
|
|
12
|
+
|
|
13
|
+
export type WebhookEventType =
|
|
14
|
+
| "SubscriptionStarted"
|
|
15
|
+
| "SubscriptionRenewed"
|
|
16
|
+
| "SubscriptionExpired"
|
|
17
|
+
| "SubscriptionInGracePeriod"
|
|
18
|
+
| "SubscriptionInBillingRetry"
|
|
19
|
+
| "SubscriptionRecovered"
|
|
20
|
+
| "SubscriptionCanceled"
|
|
21
|
+
| "SubscriptionUncanceled"
|
|
22
|
+
| "SubscriptionRevoked"
|
|
23
|
+
| "SubscriptionPriceChange"
|
|
24
|
+
| "SubscriptionProductChanged"
|
|
25
|
+
| "SubscriptionPaused"
|
|
26
|
+
| "SubscriptionResumed"
|
|
27
|
+
| "PurchaseRefunded"
|
|
28
|
+
| "PurchaseConsumptionRequest"
|
|
29
|
+
| "TestNotification";
|
|
30
|
+
|
|
31
|
+
export const WEBHOOK_EVENT_TYPES = [
|
|
32
|
+
"SubscriptionStarted",
|
|
33
|
+
"SubscriptionRenewed",
|
|
34
|
+
"SubscriptionExpired",
|
|
35
|
+
"SubscriptionInGracePeriod",
|
|
36
|
+
"SubscriptionInBillingRetry",
|
|
37
|
+
"SubscriptionRecovered",
|
|
38
|
+
"SubscriptionCanceled",
|
|
39
|
+
"SubscriptionUncanceled",
|
|
40
|
+
"SubscriptionRevoked",
|
|
41
|
+
"SubscriptionPriceChange",
|
|
42
|
+
"SubscriptionProductChanged",
|
|
43
|
+
"SubscriptionPaused",
|
|
44
|
+
"SubscriptionResumed",
|
|
45
|
+
"PurchaseRefunded",
|
|
46
|
+
"PurchaseConsumptionRequest",
|
|
47
|
+
"TestNotification",
|
|
48
|
+
] as const satisfies readonly WebhookEventType[];
|
|
49
|
+
|
|
50
|
+
export type WebhookEventPayload = {
|
|
51
|
+
id: string;
|
|
52
|
+
type: WebhookEventType;
|
|
53
|
+
source: string;
|
|
54
|
+
platform: "IOS" | "Android";
|
|
55
|
+
environment: "Production" | "Sandbox" | "Xcode";
|
|
56
|
+
projectId: string;
|
|
57
|
+
occurredAt: number;
|
|
58
|
+
receivedAt: number;
|
|
59
|
+
// Optional because TestNotification frames carry no transaction;
|
|
60
|
+
// every other event type populates this.
|
|
61
|
+
purchaseToken?: string;
|
|
62
|
+
productId?: string;
|
|
63
|
+
subscriptionState?: string;
|
|
64
|
+
expiresAt?: number;
|
|
65
|
+
renewsAt?: number;
|
|
66
|
+
cancellationReason?: string;
|
|
67
|
+
currency?: string;
|
|
68
|
+
priceAmountMicros?: number;
|
|
69
|
+
rawSignedPayload?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type WebhookListenerOptions = {
|
|
73
|
+
/**
|
|
74
|
+
* Project API key. Embedded in the URL path because Apple ASN
|
|
75
|
+
* registration cannot send custom headers; the same path is reused
|
|
76
|
+
* here for symmetry.
|
|
77
|
+
*/
|
|
78
|
+
apiKey: string;
|
|
79
|
+
/**
|
|
80
|
+
* Override the kit base URL. Defaults to https://kit.openiap.dev.
|
|
81
|
+
* In tests, point this at a local server.
|
|
82
|
+
*/
|
|
83
|
+
baseUrl?: string;
|
|
84
|
+
/** Called on every successfully-parsed webhook event. */
|
|
85
|
+
onEvent: (event: WebhookEventPayload) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Called on transport errors. The connection auto-reconnects
|
|
88
|
+
* unconditionally; this callback exists for telemetry / surfacing
|
|
89
|
+
* to the host UI.
|
|
90
|
+
*/
|
|
91
|
+
onError?: (error: WebhookListenerError) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Optional injection of an EventSource constructor. Lets RN /
|
|
94
|
+
* Expo plug in `react-native-event-source` when running on a JS
|
|
95
|
+
* runtime that lacks the global, or vitest plug in a stub.
|
|
96
|
+
*/
|
|
97
|
+
eventSourceFactory?: (
|
|
98
|
+
url: string,
|
|
99
|
+
headers: Record<string, string>,
|
|
100
|
+
) => WebhookEventStream;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export interface WebhookEventStream {
|
|
104
|
+
close(): void;
|
|
105
|
+
onmessage: ((event: { data: string; lastEventId?: string }) => void) | null;
|
|
106
|
+
onerror: ((error: unknown) => void) | null;
|
|
107
|
+
addEventListener?: (
|
|
108
|
+
type: string,
|
|
109
|
+
listener: (event: { data: string; lastEventId?: string }) => void,
|
|
110
|
+
) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type WebhookListener = {
|
|
114
|
+
/** Tear down the connection and stop receiving events. */
|
|
115
|
+
close(): void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type WebhookListenerError = {
|
|
119
|
+
code:
|
|
120
|
+
| "TRANSPORT_ERROR"
|
|
121
|
+
| "PARSE_ERROR"
|
|
122
|
+
| "MALFORMED_EVENT"
|
|
123
|
+
| "NO_EVENTSOURCE";
|
|
124
|
+
message: string;
|
|
125
|
+
cause?: unknown;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const DEFAULT_BASE_URL = "https://kit.openiap.dev";
|
|
129
|
+
|
|
130
|
+
export function connectWebhookStream(
|
|
131
|
+
options: WebhookListenerOptions,
|
|
132
|
+
): WebhookListener {
|
|
133
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
134
|
+
const url = `${trimTrailingSlash(baseUrl)}/v1/webhooks/stream/${encodeURIComponent(options.apiKey)}`;
|
|
135
|
+
|
|
136
|
+
const factory = options.eventSourceFactory ?? defaultEventSourceFactory;
|
|
137
|
+
let stream: WebhookEventStream;
|
|
138
|
+
try {
|
|
139
|
+
stream = factory(url, {});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
options.onError?.({
|
|
142
|
+
code: "NO_EVENTSOURCE",
|
|
143
|
+
message:
|
|
144
|
+
error instanceof Error
|
|
145
|
+
? error.message
|
|
146
|
+
: "EventSource constructor unavailable in this runtime",
|
|
147
|
+
cause: error,
|
|
148
|
+
});
|
|
149
|
+
return { close: () => {} };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const seenIds = new Set<string>();
|
|
153
|
+
const seenOrder: string[] = [];
|
|
154
|
+
const markSeen = (id: string): boolean => {
|
|
155
|
+
if (seenIds.has(id)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
seenIds.add(id);
|
|
159
|
+
seenOrder.push(id);
|
|
160
|
+
if (seenOrder.length > 1024) {
|
|
161
|
+
const evicted = seenOrder.shift();
|
|
162
|
+
if (evicted !== undefined) {
|
|
163
|
+
seenIds.delete(evicted);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleData = (raw: string) => {
|
|
170
|
+
const parsed = parseWebhookEventData(raw);
|
|
171
|
+
if (parsed.kind === "error") {
|
|
172
|
+
options.onError?.({
|
|
173
|
+
code: "PARSE_ERROR",
|
|
174
|
+
message: parsed.message,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (parsed.kind === "skip") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (markSeen(parsed.event.id)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
options.onEvent(parsed.event);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (typeof stream.addEventListener === "function") {
|
|
188
|
+
stream.addEventListener("message", (event) => handleData(event.data));
|
|
189
|
+
// WHATWG EventSource dispatches frames with `event: Foo` only to
|
|
190
|
+
// listeners registered for `Foo`, not to `message` / `onmessage`.
|
|
191
|
+
// Kit emits webhook frames as typed SSE events, so subscribe to
|
|
192
|
+
// every known webhook type and keep `message` for older servers or
|
|
193
|
+
// polyfills that collapse typed frames into the generic channel.
|
|
194
|
+
for (const eventType of WEBHOOK_EVENT_TYPES) {
|
|
195
|
+
stream.addEventListener(eventType, (event) => handleData(event.data));
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
stream.onmessage = (event) => handleData(event.data);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
stream.onerror = (error) => {
|
|
202
|
+
options.onError?.({
|
|
203
|
+
code: "TRANSPORT_ERROR",
|
|
204
|
+
message: "SSE transport error (auto-reconnecting)",
|
|
205
|
+
cause: error,
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
close: () => {
|
|
211
|
+
try {
|
|
212
|
+
stream.close();
|
|
213
|
+
} catch {
|
|
214
|
+
// Closing an already-closed EventSource is a no-op in browsers
|
|
215
|
+
// but throws in some polyfills.
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Pure helpers (exported for testing).
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
export type ParsedEventResult =
|
|
226
|
+
| { kind: "ok"; event: WebhookEventPayload }
|
|
227
|
+
| { kind: "skip"; reason: "heartbeat" | "stream-control" }
|
|
228
|
+
| { kind: "error"; message: string };
|
|
229
|
+
|
|
230
|
+
export function parseWebhookEventData(raw: string): ParsedEventResult {
|
|
231
|
+
if (!raw) {
|
|
232
|
+
return { kind: "skip", reason: "heartbeat" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let parsed: unknown;
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(raw);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
kind: "error",
|
|
241
|
+
message:
|
|
242
|
+
error instanceof Error
|
|
243
|
+
? `Failed to parse SSE payload: ${error.message}`
|
|
244
|
+
: "Failed to parse SSE payload",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
typeof parsed !== "object" ||
|
|
250
|
+
parsed === null ||
|
|
251
|
+
!("type" in parsed) ||
|
|
252
|
+
typeof (parsed as Record<string, unknown>).type !== "string"
|
|
253
|
+
) {
|
|
254
|
+
// Stream-control messages (the `ready`/`stream-error` envelopes
|
|
255
|
+
// emitted by the kit server) have no `type` and are surfaced as
|
|
256
|
+
// skips so consumers don't see them as events.
|
|
257
|
+
return { kind: "skip", reason: "stream-control" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const event = parsed as WebhookEventPayload;
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
typeof event.id !== "string" ||
|
|
264
|
+
typeof event.occurredAt !== "number" ||
|
|
265
|
+
typeof event.receivedAt !== "number"
|
|
266
|
+
) {
|
|
267
|
+
return {
|
|
268
|
+
kind: "error",
|
|
269
|
+
message: `WebhookEvent missing required fields (id/occurredAt/receivedAt)`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// purchaseToken is required for every event type *except*
|
|
273
|
+
// TestNotification — Apple ASN v2 / Google RTDN test payloads
|
|
274
|
+
// carry no transaction. Hard-rejecting here would surface valid
|
|
275
|
+
// test webhooks as MALFORMED_EVENT and never reach listeners.
|
|
276
|
+
if (
|
|
277
|
+
event.type !== "TestNotification" &&
|
|
278
|
+
typeof event.purchaseToken !== "string"
|
|
279
|
+
) {
|
|
280
|
+
return {
|
|
281
|
+
kind: "error",
|
|
282
|
+
message: `WebhookEvent missing required field purchaseToken`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { kind: "ok", event };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function trimTrailingSlash(url: string): string {
|
|
290
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function defaultEventSourceFactory(
|
|
294
|
+
url: string,
|
|
295
|
+
_headers: Record<string, string>,
|
|
296
|
+
): WebhookEventStream {
|
|
297
|
+
// EventSource is part of the WHATWG spec and available in all
|
|
298
|
+
// browser environments and most JS runtimes (Bun, Node 22+, Deno).
|
|
299
|
+
// RN does not ship it natively — consumers must pass
|
|
300
|
+
// `eventSourceFactory` from `react-native-sse` or similar.
|
|
301
|
+
const ctor = (
|
|
302
|
+
globalThis as {
|
|
303
|
+
EventSource?: new (url: string) => WebhookEventStream;
|
|
304
|
+
}
|
|
305
|
+
).EventSource;
|
|
306
|
+
if (!ctor) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
"EventSource is not defined. Pass `eventSourceFactory` for runtimes without a built-in EventSource.",
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return new ctor(url);
|
|
312
|
+
}
|