@dynamic-labs-sdk/react-native-captcha 0.0.0 → 1.27.1

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/dist/index.cjs ADDED
@@ -0,0 +1,434 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let _dynamic_labs_sdk_assert_package_version = require("@dynamic-labs-sdk/assert-package-version");
29
+ let react = require("react");
30
+ let react_native = require("react-native");
31
+ let react_native_webview = require("react-native-webview");
32
+ react_native_webview = __toESM(react_native_webview);
33
+ let _dynamic_labs_sdk_client_core = require("@dynamic-labs-sdk/client/core");
34
+ let zod_mini = require("zod/mini");
35
+ zod_mini = __toESM(zod_mini);
36
+ let _dynamic_labs_sdk_client = require("@dynamic-labs-sdk/client");
37
+ let react_jsx_runtime = require("react/jsx-runtime");
38
+
39
+ //#region package.json
40
+ var name = "@dynamic-labs-sdk/react-native-captcha";
41
+ var version = "1.27.1";
42
+
43
+ //#endregion
44
+ //#region src/errors/HCaptchaError.ts
45
+ /**
46
+ * Thrown when the embedded hCaptcha challenge fails to resolve — for example
47
+ * the widget reports an error, the token expires, or the user closes the
48
+ * challenge without completing it.
49
+ *
50
+ * The {@link HCaptchaChallenge} component surfaces this typed error through its
51
+ * `onError` callback so consumers can distinguish a failed captcha from other
52
+ * failures and prompt the user to retry rather than treating it as a fatal
53
+ * error.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * <HCaptchaChallenge
58
+ * onError={(error) => {
59
+ * if (error instanceof HCaptchaError) {
60
+ * promptRetry(error.message);
61
+ * }
62
+ * }}
63
+ * />
64
+ * ```
65
+ * @see HCaptchaChallenge
66
+ * @see HCaptchaChallengeProps
67
+ */
68
+ var HCaptchaError = class extends _dynamic_labs_sdk_client.BaseError {
69
+ constructor({ description }) {
70
+ super({
71
+ cause: null,
72
+ code: "hcaptcha_failed",
73
+ docsUrl: null,
74
+ name: "HCaptchaError",
75
+ shortMessage: `hCaptcha failed: ${description ?? "unknown error"}`
76
+ });
77
+ }
78
+ };
79
+
80
+ //#endregion
81
+ //#region src/components/HCaptchaChallenge/HCaptchaChallenge.tsx
82
+ /**
83
+ * `testID` e2e drivers use to locate the hCaptcha widget on screen.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * <View testID={HCAPTCHA_CHALLENGE_TEST_ID} />
88
+ * ```
89
+ * @see HCaptchaChallenge
90
+ * @see HCaptchaChallengeProps
91
+ */
92
+ const HCAPTCHA_CHALLENGE_TEST_ID = "hcaptcha-challenge";
93
+ const bridgeMessageSchema$1 = zod_mini.union([zod_mini.object({
94
+ message: zod_mini.optional(zod_mini.string()),
95
+ type: zod_mini.enum(["error"])
96
+ }), zod_mini.object({
97
+ token: zod_mini.string(),
98
+ type: zod_mini.enum(["token"])
99
+ })]);
100
+ const sanitizeSiteKey$1 = (siteKey) => siteKey.replace(/[^\w-]/g, "");
101
+ const buildHCaptchaHtml = (siteKey) => `
102
+ <!DOCTYPE html>
103
+ <html>
104
+ <head>
105
+ <meta name="viewport" content="width=device-width, initial-scale=1">
106
+ <!-- No Subresource Integrity: hCaptcha serves a mutable loader script and
107
+ does not support SRI pinning; a pinned hash would break on their next
108
+ deploy. The script loads from hCaptcha's own origin over HTTPS. -->
109
+ <script src="https://js.hcaptcha.com/1/api.js" async defer><\/script>
110
+ </head>
111
+ <body style="margin:0;padding:0;display:flex;justify-content:center;align-items:center;min-height:100vh;">
112
+ <div
113
+ class="h-captcha"
114
+ data-sitekey="${siteKey}"
115
+ data-callback="onSuccess"
116
+ data-error-callback="onChallengeError"
117
+ data-expired-callback="onExpired"
118
+ ></div>
119
+ <script>
120
+ const post = (payload) => {
121
+ window.ReactNativeWebView.postMessage(JSON.stringify(payload));
122
+ };
123
+ // hCaptcha resolves the data-*-callback attributes by name off the global
124
+ // scope, so these handlers must be assigned onto window — a bare const
125
+ // binding would not be reachable and the callbacks would never fire.
126
+ window.onSuccess = (token) => {
127
+ post({ type: 'token', token: token });
128
+ };
129
+ window.onChallengeError = (error) => {
130
+ // Cap the hCaptcha-supplied error before forwarding it: it only ends up
131
+ // in an HCaptchaError message, so bound its length rather than surface an
132
+ // arbitrarily long string from the widget.
133
+ const errorMessage = String(error).substring(0, 200);
134
+ post({ type: 'error', message: 'challenge error: ' + errorMessage });
135
+ };
136
+ window.onExpired = () => {
137
+ post({ type: 'error', message: 'token expired before it was claimed' });
138
+ };
139
+ <\/script>
140
+ </body>
141
+ </html>
142
+ `;
143
+ /**
144
+ * Renders the hCaptcha checkbox widget in an inline WebView and reports the
145
+ * resulting captcha token over the postMessage bridge. The caller hands the
146
+ * token to the SDK's `setCaptchaToken`. Exactly one of `onToken`/`onError`
147
+ * fires, exactly once — the first terminal event (token, challenge error,
148
+ * expiry, or WebView load failure) wins.
149
+ *
150
+ * @example
151
+ * ```tsx
152
+ * <HCaptchaChallenge
153
+ * baseUrl="https://your-whitelisted-origin.com"
154
+ * siteKey="10000000-ffff-ffff-ffff-000000000001"
155
+ * onToken={(token) => setCaptchaToken({ captchaToken: token })}
156
+ * onError={(error) => console.error(error.message)}
157
+ * style={{ height: 260 }}
158
+ * logLevel="warn"
159
+ * />
160
+ * ```
161
+ * @returns A React Native view that hosts the captcha WebView.
162
+ * @see HCaptchaChallengeProps
163
+ * @see HCaptchaError
164
+ */
165
+ const HCaptchaChallenge = ({ baseUrl, logLevel, onError, onToken, siteKey, style }) => {
166
+ const settledRef = (0, react.useRef)(false);
167
+ const logger = (0, react.useMemo)(() => (0, _dynamic_labs_sdk_client_core.createLogger)({ level: logLevel ?? "warn" }), [logLevel]);
168
+ const settleWith = (dispatch) => {
169
+ if (settledRef.current) return;
170
+ settledRef.current = true;
171
+ dispatch();
172
+ };
173
+ const handleMessage = (event) => {
174
+ let data;
175
+ try {
176
+ data = JSON.parse(event.nativeEvent.data);
177
+ } catch (error) {
178
+ logger.debug("Malformed hCaptcha bridge message", {
179
+ error: error instanceof Error ? error.message : String(error),
180
+ rawData: String(event.nativeEvent.data).slice(0, 200)
181
+ });
182
+ return;
183
+ }
184
+ const parsed = bridgeMessageSchema$1.safeParse(data);
185
+ if (!parsed.success) {
186
+ logger.debug("Malformed hCaptcha bridge message", {
187
+ error: parsed.error.message,
188
+ rawData: String(event.nativeEvent.data).slice(0, 200)
189
+ });
190
+ return;
191
+ }
192
+ if (parsed.data.type === "token") {
193
+ const token = parsed.data.token;
194
+ settleWith(() => onToken(token));
195
+ } else {
196
+ const message = parsed.data.message;
197
+ settleWith(() => onError(new HCaptchaError({ description: message })));
198
+ }
199
+ };
200
+ const handleLoadError = (event) => {
201
+ settleWith(() => onError(new HCaptchaError({ description: `WebView failed to load: ${event.nativeEvent.description ?? "unknown error"}` })));
202
+ };
203
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
204
+ style: [styles$1.container, style],
205
+ testID: HCAPTCHA_CHALLENGE_TEST_ID,
206
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_webview.default, {
207
+ domStorageEnabled: true,
208
+ javaScriptEnabled: true,
209
+ thirdPartyCookiesEnabled: true,
210
+ onError: handleLoadError,
211
+ onHttpError: handleLoadError,
212
+ onMessage: handleMessage,
213
+ originWhitelist: ["https://*"],
214
+ source: {
215
+ baseUrl,
216
+ html: buildHCaptchaHtml(sanitizeSiteKey$1(siteKey))
217
+ },
218
+ style: styles$1.webView
219
+ })
220
+ });
221
+ };
222
+ const styles$1 = react_native.StyleSheet.create({
223
+ container: {
224
+ height: 260,
225
+ overflow: "hidden"
226
+ },
227
+ webView: {
228
+ backgroundColor: "transparent",
229
+ flex: 1
230
+ }
231
+ });
232
+
233
+ //#endregion
234
+ //#region src/errors/TurnstileError.ts
235
+ /**
236
+ * Thrown when the embedded Cloudflare Turnstile challenge fails to resolve —
237
+ * for example the widget reports an error, the token expires, or the user
238
+ * closes the challenge without completing it.
239
+ *
240
+ * The {@link TurnstileChallenge} component surfaces this typed error through
241
+ * its `onError` callback so consumers can distinguish a failed captcha from
242
+ * other failures and prompt the user to retry rather than treating it as a
243
+ * fatal error.
244
+ *
245
+ * @example
246
+ * ```tsx
247
+ * <TurnstileChallenge
248
+ * onError={(error) => {
249
+ * if (error instanceof TurnstileError) {
250
+ * promptRetry(error.message);
251
+ * }
252
+ * }}
253
+ * />
254
+ * ```
255
+ * @see TurnstileChallenge
256
+ * @see TurnstileChallengeProps
257
+ */
258
+ var TurnstileError = class extends _dynamic_labs_sdk_client.BaseError {
259
+ constructor({ description }) {
260
+ super({
261
+ cause: null,
262
+ code: "turnstile_failed",
263
+ docsUrl: null,
264
+ name: "TurnstileError",
265
+ shortMessage: `Turnstile failed: ${description ?? "unknown error"}`
266
+ });
267
+ }
268
+ };
269
+
270
+ //#endregion
271
+ //#region src/components/TurnstileChallenge/TurnstileChallenge.tsx
272
+ /**
273
+ * `testID` e2e drivers use to locate the Turnstile widget on screen.
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * <View testID={TURNSTILE_CHALLENGE_TEST_ID} />
278
+ * ```
279
+ * @see TurnstileChallenge
280
+ * @see TurnstileChallengeProps
281
+ */
282
+ const TURNSTILE_CHALLENGE_TEST_ID = "turnstile-challenge";
283
+ const bridgeMessageSchema = zod_mini.union([zod_mini.object({
284
+ message: zod_mini.optional(zod_mini.string()),
285
+ type: zod_mini.enum(["error"])
286
+ }), zod_mini.object({
287
+ token: zod_mini.string(),
288
+ type: zod_mini.enum(["token"])
289
+ })]);
290
+ const sanitizeSiteKey = (siteKey) => siteKey.replace(/[^\w-]/g, "");
291
+ const buildTurnstileHtml = (siteKey) => `
292
+ <!DOCTYPE html>
293
+ <html>
294
+ <head>
295
+ <meta name="viewport" content="width=device-width, initial-scale=1">
296
+ <!-- No Subresource Integrity: Cloudflare serves a mutable loader script and
297
+ does not support SRI pinning; a pinned hash would break on their next
298
+ deploy. The script loads from Cloudflare's own origin over HTTPS. -->
299
+ <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer><\/script>
300
+ </head>
301
+ <body style="margin:0;padding:0;display:flex;justify-content:center;align-items:center;min-height:100vh;">
302
+ <div
303
+ class="cf-turnstile"
304
+ data-sitekey="${siteKey}"
305
+ data-callback="onSuccess"
306
+ data-error-callback="onError"
307
+ data-expired-callback="onExpired"
308
+ ></div>
309
+ <script>
310
+ const post = (payload) => {
311
+ window.ReactNativeWebView.postMessage(JSON.stringify(payload));
312
+ };
313
+ // Turnstile resolves the data-*-callback attributes by name off the global
314
+ // scope, so these handlers must be assigned onto window — a bare const
315
+ // binding would not be reachable and the callbacks would never fire.
316
+ window.onSuccess = (token) => {
317
+ post({ type: 'token', token: token });
318
+ };
319
+ window.onError = (error) => {
320
+ // Cap the Turnstile-supplied error before forwarding it: it only ends up
321
+ // in a TurnstileError message, so bound its length rather than surface an
322
+ // arbitrarily long string from the widget.
323
+ const errorMessage = String(error).substring(0, 200);
324
+ post({ type: 'error', message: 'challenge error: ' + errorMessage });
325
+ };
326
+ window.onExpired = () => {
327
+ post({ type: 'error', message: 'token expired before it was claimed' });
328
+ };
329
+ <\/script>
330
+ </body>
331
+ </html>
332
+ `;
333
+ /**
334
+ * Renders the Cloudflare Turnstile widget in an inline WebView and reports the
335
+ * resulting captcha token over the postMessage bridge. The caller hands the
336
+ * token to the SDK's `setCaptchaToken`. Exactly one of `onToken`/`onError`
337
+ * fires, exactly once — the first terminal event (token, challenge error,
338
+ * expiry, or WebView load failure) wins.
339
+ *
340
+ * @example
341
+ * ```tsx
342
+ * <TurnstileChallenge
343
+ * baseUrl="https://your-whitelisted-origin.com"
344
+ * siteKey="1x00000000000000000000AA"
345
+ * onToken={(token) => setCaptchaToken({ captchaToken: token })}
346
+ * onError={(error) => console.error(error.message)}
347
+ * style={{ height: 120 }}
348
+ * logLevel="warn"
349
+ * />
350
+ * ```
351
+ * @returns A React Native view that hosts the captcha WebView.
352
+ * @see TurnstileChallengeProps
353
+ * @see TurnstileError
354
+ */
355
+ const TurnstileChallenge = ({ baseUrl, logLevel, onError, onToken, siteKey, style }) => {
356
+ const settledRef = (0, react.useRef)(false);
357
+ const logger = (0, react.useMemo)(() => (0, _dynamic_labs_sdk_client_core.createLogger)({ level: logLevel ?? "warn" }), [logLevel]);
358
+ const settleWith = (dispatch) => {
359
+ if (settledRef.current) return;
360
+ settledRef.current = true;
361
+ dispatch();
362
+ };
363
+ const handleMessage = (event) => {
364
+ let data;
365
+ try {
366
+ data = JSON.parse(event.nativeEvent.data);
367
+ } catch (error) {
368
+ logger.debug("Malformed Turnstile bridge message", {
369
+ error: error instanceof Error ? error.message : String(error),
370
+ rawData: String(event.nativeEvent.data).slice(0, 200)
371
+ });
372
+ return;
373
+ }
374
+ const parsed = bridgeMessageSchema.safeParse(data);
375
+ if (!parsed.success) {
376
+ logger.debug("Malformed Turnstile bridge message", {
377
+ error: parsed.error.message,
378
+ rawData: String(event.nativeEvent.data).slice(0, 200)
379
+ });
380
+ return;
381
+ }
382
+ if (parsed.data.type === "token") {
383
+ const token = parsed.data.token;
384
+ settleWith(() => onToken(token));
385
+ } else {
386
+ const message = parsed.data.message;
387
+ settleWith(() => onError(new TurnstileError({ description: message })));
388
+ }
389
+ };
390
+ const handleLoadError = (event) => {
391
+ settleWith(() => onError(new TurnstileError({ description: `WebView failed to load: ${event.nativeEvent.description ?? "unknown error"}` })));
392
+ };
393
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
394
+ style: [styles.container, style],
395
+ testID: TURNSTILE_CHALLENGE_TEST_ID,
396
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_webview.default, {
397
+ domStorageEnabled: true,
398
+ javaScriptEnabled: true,
399
+ thirdPartyCookiesEnabled: true,
400
+ onError: handleLoadError,
401
+ onHttpError: handleLoadError,
402
+ onMessage: handleMessage,
403
+ originWhitelist: ["https://*", "about:srcdoc"],
404
+ source: {
405
+ baseUrl,
406
+ html: buildTurnstileHtml(sanitizeSiteKey(siteKey))
407
+ },
408
+ style: styles.webView
409
+ })
410
+ });
411
+ };
412
+ const styles = react_native.StyleSheet.create({
413
+ container: {
414
+ height: 120,
415
+ overflow: "hidden"
416
+ },
417
+ webView: {
418
+ backgroundColor: "transparent",
419
+ flex: 1
420
+ }
421
+ });
422
+
423
+ //#endregion
424
+ //#region src/exports/index.ts
425
+ (0, _dynamic_labs_sdk_assert_package_version.assertPackageVersion)(name, version);
426
+
427
+ //#endregion
428
+ exports.HCAPTCHA_CHALLENGE_TEST_ID = HCAPTCHA_CHALLENGE_TEST_ID;
429
+ exports.HCaptchaChallenge = HCaptchaChallenge;
430
+ exports.HCaptchaError = HCaptchaError;
431
+ exports.TURNSTILE_CHALLENGE_TEST_ID = TURNSTILE_CHALLENGE_TEST_ID;
432
+ exports.TurnstileChallenge = TurnstileChallenge;
433
+ exports.TurnstileError = TurnstileError;
434
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["BaseError","bridgeMessageSchema","z","sanitizeSiteKey","HCaptchaChallenge: FC<HCaptchaChallengeProps>","data: unknown","View","styles","WebView","StyleSheet","BaseError","z","TurnstileChallenge: FC<TurnstileChallengeProps>","data: unknown","View","WebView","StyleSheet","packageName","packageVersion"],"sources":["../package.json","../src/errors/HCaptchaError.ts","../src/components/HCaptchaChallenge/HCaptchaChallenge.tsx","../src/errors/TurnstileError.ts","../src/components/TurnstileChallenge/TurnstileChallenge.tsx","../src/exports/index.ts"],"sourcesContent":["","import { BaseError } from '@dynamic-labs-sdk/client';\n\n/**\n * Thrown when the embedded hCaptcha challenge fails to resolve — for example\n * the widget reports an error, the token expires, or the user closes the\n * challenge without completing it.\n *\n * The {@link HCaptchaChallenge} component surfaces this typed error through its\n * `onError` callback so consumers can distinguish a failed captcha from other\n * failures and prompt the user to retry rather than treating it as a fatal\n * error.\n *\n * @example\n * ```tsx\n * <HCaptchaChallenge\n * onError={(error) => {\n * if (error instanceof HCaptchaError) {\n * promptRetry(error.message);\n * }\n * }}\n * />\n * ```\n * @see HCaptchaChallenge\n * @see HCaptchaChallengeProps\n */\nexport class HCaptchaError extends BaseError {\n constructor({ description }: { description: string | undefined }) {\n super({\n cause: null,\n code: 'hcaptcha_failed',\n docsUrl: null,\n name: 'HCaptchaError',\n shortMessage: `hCaptcha failed: ${description ?? 'unknown error'}`,\n });\n }\n}\n","'use client';\n\nimport type { FC } from 'react';\nimport { useMemo, useRef } from 'react';\nimport type { StyleProp, ViewStyle } from 'react-native';\nimport { StyleSheet, View } from 'react-native';\nimport type { WebViewMessageEvent } from 'react-native-webview';\nimport WebView from 'react-native-webview';\nimport { type LogLevel, createLogger } from '@dynamic-labs-sdk/client/core';\nimport * as z from 'zod/mini';\n\nimport { HCaptchaError } from '../../errors/HCaptchaError';\n\n/**\n * `testID` e2e drivers use to locate the hCaptcha widget on screen.\n *\n * @example\n * ```tsx\n * <View testID={HCAPTCHA_CHALLENGE_TEST_ID} />\n * ```\n * @see HCaptchaChallenge\n * @see HCaptchaChallengeProps\n */\nexport const HCAPTCHA_CHALLENGE_TEST_ID = 'hcaptcha-challenge';\n\n// react-native-webview only exports the message event type from its root;\n// for error events this is the minimal shape both onError and onHttpError\n// deliver.\ntype WebViewErrorEvent = { nativeEvent: { description?: string } };\n\n// Payload the widget document posts over the bridge (see buildHCaptchaHtml).\nconst bridgeMessageSchema = z.union([\n z.object({\n message: z.optional(z.string()),\n type: z.enum(['error']),\n }),\n z.object({\n token: z.string(),\n type: z.enum(['token']),\n }),\n]);\n\n/**\n * Props for the {@link HCaptchaChallenge} component.\n *\n * @example\n * ```tsx\n * const props: HCaptchaChallengeProps = {\n * baseUrl: 'https://your-whitelisted-origin.com',\n * onError: console.error,\n * onToken: setToken,\n * siteKey: '10000000-ffff-ffff-ffff-000000000001',\n * logLevel: 'warn',\n * };\n * ```\n * @see HCaptchaChallenge\n * @see HCaptchaError\n */\nexport type HCaptchaChallengeProps = {\n /**\n * Origin the widget document is served under. hCaptcha refuses to render on\n * origin-less documents, and real (non-test) site keys only render on hosts\n * whitelisted for the key — pass an origin your site key accepts.\n */\n baseUrl: string;\n /**\n * Optional minimum log level for the component's internal logger. Defaults\n * to `'warn'` when omitted.\n */\n logLevel?: LogLevel;\n /**\n * Called when the widget cannot produce a token: the WebView fails to load,\n * hCaptcha reports a challenge error, or the token expires unclaimed —\n * so callers awaiting `onToken` fail immediately instead of hanging until\n * their own timeout.\n */\n onError: (error: HCaptchaError) => void;\n /** Called with the token hCaptcha issues when the widget is solved. */\n onToken: (token: string) => void;\n /**\n * The environment's hCaptcha site key, from the SDK's `getCaptchaSettings`.\n */\n siteKey: string;\n /**\n * Optional style for the outer container. Use this to set width/height,\n * margins, border radius, or background color to match your app's UI.\n */\n style?: StyleProp<ViewStyle>;\n};\n\n// The site key is server-controlled, but it lands inside an HTML attribute —\n// keep only the charset real hCaptcha site keys use so a misconfigured value\n// cannot break out of the markup.\nconst sanitizeSiteKey = (siteKey: string) => siteKey.replace(/[^\\w-]/g, '');\n\nconst buildHCaptchaHtml = (siteKey: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <!-- No Subresource Integrity: hCaptcha serves a mutable loader script and\n does not support SRI pinning; a pinned hash would break on their next\n deploy. The script loads from hCaptcha's own origin over HTTPS. -->\n <script src=\"https://js.hcaptcha.com/1/api.js\" async defer></script>\n</head>\n<body style=\"margin:0;padding:0;display:flex;justify-content:center;align-items:center;min-height:100vh;\">\n <div\n class=\"h-captcha\"\n data-sitekey=\"${siteKey}\"\n data-callback=\"onSuccess\"\n data-error-callback=\"onChallengeError\"\n data-expired-callback=\"onExpired\"\n ></div>\n <script>\n const post = (payload) => {\n window.ReactNativeWebView.postMessage(JSON.stringify(payload));\n };\n // hCaptcha resolves the data-*-callback attributes by name off the global\n // scope, so these handlers must be assigned onto window — a bare const\n // binding would not be reachable and the callbacks would never fire.\n window.onSuccess = (token) => {\n post({ type: 'token', token: token });\n };\n window.onChallengeError = (error) => {\n // Cap the hCaptcha-supplied error before forwarding it: it only ends up\n // in an HCaptchaError message, so bound its length rather than surface an\n // arbitrarily long string from the widget.\n const errorMessage = String(error).substring(0, 200);\n post({ type: 'error', message: 'challenge error: ' + errorMessage });\n };\n window.onExpired = () => {\n post({ type: 'error', message: 'token expired before it was claimed' });\n };\n </script>\n</body>\n</html>\n`;\n\n/**\n * Renders the hCaptcha checkbox widget in an inline WebView and reports the\n * resulting captcha token over the postMessage bridge. The caller hands the\n * token to the SDK's `setCaptchaToken`. Exactly one of `onToken`/`onError`\n * fires, exactly once — the first terminal event (token, challenge error,\n * expiry, or WebView load failure) wins.\n *\n * @example\n * ```tsx\n * <HCaptchaChallenge\n * baseUrl=\"https://your-whitelisted-origin.com\"\n * siteKey=\"10000000-ffff-ffff-ffff-000000000001\"\n * onToken={(token) => setCaptchaToken({ captchaToken: token })}\n * onError={(error) => console.error(error.message)}\n * style={{ height: 260 }}\n * logLevel=\"warn\"\n * />\n * ```\n * @returns A React Native view that hosts the captcha WebView.\n * @see HCaptchaChallengeProps\n * @see HCaptchaError\n */\nexport const HCaptchaChallenge: FC<HCaptchaChallengeProps> = ({\n baseUrl,\n logLevel,\n onError,\n onToken,\n siteKey,\n style,\n}) => {\n // First terminal event wins: hCaptcha only issues one token per solve, but\n // guard against duplicate bridge messages and double error events (onError\n // and onHttpError can both fire for one failure) so the callbacks keep\n // their called-once contract.\n const settledRef = useRef(false);\n\n const logger = useMemo(\n () => createLogger({ level: logLevel ?? 'warn' }),\n [logLevel]\n );\n\n // eslint-disable-next-line custom-rules/require-single-object-param -- (dispatch) is the entire input of this local helper\n const settleWith = (dispatch: () => void) => {\n if (settledRef.current) {\n return;\n }\n\n settledRef.current = true;\n dispatch();\n };\n\n const handleMessage = (event: WebViewMessageEvent) => {\n let data: unknown;\n try {\n data = JSON.parse(event.nativeEvent.data);\n } catch (error) {\n logger.debug('Malformed hCaptcha bridge message', {\n error: error instanceof Error ? error.message : String(error),\n rawData: String(event.nativeEvent.data).slice(0, 200),\n });\n return;\n }\n\n const parsed = bridgeMessageSchema.safeParse(data);\n if (!parsed.success) {\n logger.debug('Malformed hCaptcha bridge message', {\n error: parsed.error.message,\n rawData: String(event.nativeEvent.data).slice(0, 200),\n });\n return;\n }\n\n if (parsed.data.type === 'token') {\n const token = parsed.data.token;\n settleWith(() => onToken(token));\n } else {\n const message = parsed.data.message;\n settleWith(() => onError(new HCaptchaError({ description: message })));\n }\n };\n\n const handleLoadError = (event: WebViewErrorEvent) => {\n settleWith(() =>\n onError(\n new HCaptchaError({\n description: `WebView failed to load: ${event.nativeEvent.description ?? 'unknown error'}`,\n })\n )\n );\n };\n\n return (\n <View style={[styles.container, style]} testID={HCAPTCHA_CHALLENGE_TEST_ID}>\n <WebView\n domStorageEnabled\n javaScriptEnabled\n thirdPartyCookiesEnabled\n onError={handleLoadError}\n onHttpError={handleLoadError}\n onMessage={handleMessage}\n // https-only: hCaptcha's iframes may come from several hosts, but a\n // custom-scheme navigation escaping the WebView is never legitimate.\n originWhitelist={['https://*']}\n source={{\n baseUrl,\n html: buildHCaptchaHtml(sanitizeSiteKey(siteKey)),\n }}\n style={styles.webView}\n />\n </View>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n // Tall enough for the checkbox widget plus the verification animation;\n // the challenge popup never appears with the always-pass test site key.\n height: 260,\n overflow: 'hidden',\n },\n webView: {\n backgroundColor: 'transparent',\n flex: 1,\n },\n});\n","import { BaseError } from '@dynamic-labs-sdk/client';\n\n/**\n * Thrown when the embedded Cloudflare Turnstile challenge fails to resolve —\n * for example the widget reports an error, the token expires, or the user\n * closes the challenge without completing it.\n *\n * The {@link TurnstileChallenge} component surfaces this typed error through\n * its `onError` callback so consumers can distinguish a failed captcha from\n * other failures and prompt the user to retry rather than treating it as a\n * fatal error.\n *\n * @example\n * ```tsx\n * <TurnstileChallenge\n * onError={(error) => {\n * if (error instanceof TurnstileError) {\n * promptRetry(error.message);\n * }\n * }}\n * />\n * ```\n * @see TurnstileChallenge\n * @see TurnstileChallengeProps\n */\nexport class TurnstileError extends BaseError {\n constructor({ description }: { description: string | undefined }) {\n super({\n cause: null,\n code: 'turnstile_failed',\n docsUrl: null,\n name: 'TurnstileError',\n shortMessage: `Turnstile failed: ${description ?? 'unknown error'}`,\n });\n }\n}\n","'use client';\n\nimport type { FC } from 'react';\nimport { useMemo, useRef } from 'react';\nimport type { StyleProp, ViewStyle } from 'react-native';\nimport { StyleSheet, View } from 'react-native';\nimport type { WebViewMessageEvent } from 'react-native-webview';\nimport WebView from 'react-native-webview';\nimport { type LogLevel, createLogger } from '@dynamic-labs-sdk/client/core';\nimport * as z from 'zod/mini';\n\nimport { TurnstileError } from '../../errors/TurnstileError';\n\n/**\n * `testID` e2e drivers use to locate the Turnstile widget on screen.\n *\n * @example\n * ```tsx\n * <View testID={TURNSTILE_CHALLENGE_TEST_ID} />\n * ```\n * @see TurnstileChallenge\n * @see TurnstileChallengeProps\n */\nexport const TURNSTILE_CHALLENGE_TEST_ID = 'turnstile-challenge';\n\n// react-native-webview only exports the message event type from its root;\n// for error events this is the minimal shape both onError and onHttpError\n// deliver.\ntype WebViewErrorEvent = { nativeEvent: { description?: string } };\n\n// Payload the widget document posts over the bridge (see buildTurnstileHtml).\nconst bridgeMessageSchema = z.union([\n z.object({\n message: z.optional(z.string()),\n type: z.enum(['error']),\n }),\n z.object({\n token: z.string(),\n type: z.enum(['token']),\n }),\n]);\n\n/**\n * Props for the {@link TurnstileChallenge} component.\n *\n * @example\n * ```tsx\n * const props: TurnstileChallengeProps = {\n * baseUrl: 'https://your-whitelisted-origin.com',\n * onError: console.error,\n * onToken: setToken,\n * siteKey: '1x00000000000000000000AA',\n * logLevel: 'warn',\n * };\n * ```\n * @see TurnstileChallenge\n * @see TurnstileError\n */\nexport type TurnstileChallengeProps = {\n /**\n * Origin the widget document is served under. Turnstile refuses to render on\n * origin-less documents, and real (non-test) site keys only render on hosts\n * whitelisted for the key — pass an origin your site key accepts.\n */\n baseUrl: string;\n /**\n * Optional minimum log level for the component's internal logger. Defaults\n * to `'warn'` when omitted.\n */\n logLevel?: LogLevel;\n /**\n * Called when the widget cannot produce a token: the WebView fails to load,\n * Turnstile reports a challenge error, or the token expires unclaimed —\n * so callers awaiting `onToken` fail immediately instead of hanging until\n * their own timeout.\n */\n onError: (error: TurnstileError) => void;\n /** Called with the token Turnstile issues when the widget is solved. */\n onToken: (token: string) => void;\n /**\n * The environment's Turnstile site key, from the SDK's `getCaptchaSettings`.\n */\n siteKey: string;\n /**\n * Optional style for the outer container. Use this to set width/height,\n * margins, border radius, or background color to match your app's UI.\n */\n style?: StyleProp<ViewStyle>;\n};\n\n// The site key is server-controlled, but it lands inside an HTML attribute —\n// keep only the charset real Turnstile site keys use so a misconfigured value\n// cannot break out of the markup.\nconst sanitizeSiteKey = (siteKey: string) => siteKey.replace(/[^\\w-]/g, '');\n\nconst buildTurnstileHtml = (siteKey: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <!-- No Subresource Integrity: Cloudflare serves a mutable loader script and\n does not support SRI pinning; a pinned hash would break on their next\n deploy. The script loads from Cloudflare's own origin over HTTPS. -->\n <script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n</head>\n<body style=\"margin:0;padding:0;display:flex;justify-content:center;align-items:center;min-height:100vh;\">\n <div\n class=\"cf-turnstile\"\n data-sitekey=\"${siteKey}\"\n data-callback=\"onSuccess\"\n data-error-callback=\"onError\"\n data-expired-callback=\"onExpired\"\n ></div>\n <script>\n const post = (payload) => {\n window.ReactNativeWebView.postMessage(JSON.stringify(payload));\n };\n // Turnstile resolves the data-*-callback attributes by name off the global\n // scope, so these handlers must be assigned onto window — a bare const\n // binding would not be reachable and the callbacks would never fire.\n window.onSuccess = (token) => {\n post({ type: 'token', token: token });\n };\n window.onError = (error) => {\n // Cap the Turnstile-supplied error before forwarding it: it only ends up\n // in a TurnstileError message, so bound its length rather than surface an\n // arbitrarily long string from the widget.\n const errorMessage = String(error).substring(0, 200);\n post({ type: 'error', message: 'challenge error: ' + errorMessage });\n };\n window.onExpired = () => {\n post({ type: 'error', message: 'token expired before it was claimed' });\n };\n </script>\n</body>\n</html>\n`;\n\n/**\n * Renders the Cloudflare Turnstile widget in an inline WebView and reports the\n * resulting captcha token over the postMessage bridge. The caller hands the\n * token to the SDK's `setCaptchaToken`. Exactly one of `onToken`/`onError`\n * fires, exactly once — the first terminal event (token, challenge error,\n * expiry, or WebView load failure) wins.\n *\n * @example\n * ```tsx\n * <TurnstileChallenge\n * baseUrl=\"https://your-whitelisted-origin.com\"\n * siteKey=\"1x00000000000000000000AA\"\n * onToken={(token) => setCaptchaToken({ captchaToken: token })}\n * onError={(error) => console.error(error.message)}\n * style={{ height: 120 }}\n * logLevel=\"warn\"\n * />\n * ```\n * @returns A React Native view that hosts the captcha WebView.\n * @see TurnstileChallengeProps\n * @see TurnstileError\n */\nexport const TurnstileChallenge: FC<TurnstileChallengeProps> = ({\n baseUrl,\n logLevel,\n onError,\n onToken,\n siteKey,\n style,\n}) => {\n // First terminal event wins: Turnstile only issues one token per solve, but\n // guard against duplicate bridge messages and double error events (onError\n // and onHttpError can both fire for one failure) so the callbacks keep\n // their called-once contract.\n const settledRef = useRef(false);\n\n const logger = useMemo(\n () => createLogger({ level: logLevel ?? 'warn' }),\n [logLevel]\n );\n\n // eslint-disable-next-line custom-rules/require-single-object-param -- (dispatch) is the entire input of this local helper\n const settleWith = (dispatch: () => void) => {\n if (settledRef.current) {\n return;\n }\n\n settledRef.current = true;\n dispatch();\n };\n\n const handleMessage = (event: WebViewMessageEvent) => {\n let data: unknown;\n try {\n data = JSON.parse(event.nativeEvent.data);\n } catch (error) {\n logger.debug('Malformed Turnstile bridge message', {\n error: error instanceof Error ? error.message : String(error),\n rawData: String(event.nativeEvent.data).slice(0, 200),\n });\n return;\n }\n\n const parsed = bridgeMessageSchema.safeParse(data);\n if (!parsed.success) {\n logger.debug('Malformed Turnstile bridge message', {\n error: parsed.error.message,\n rawData: String(event.nativeEvent.data).slice(0, 200),\n });\n return;\n }\n\n if (parsed.data.type === 'token') {\n const token = parsed.data.token;\n settleWith(() => onToken(token));\n } else {\n const message = parsed.data.message;\n settleWith(() => onError(new TurnstileError({ description: message })));\n }\n };\n\n const handleLoadError = (event: WebViewErrorEvent) => {\n settleWith(() =>\n onError(\n new TurnstileError({\n description: `WebView failed to load: ${event.nativeEvent.description ?? 'unknown error'}`,\n })\n )\n );\n };\n\n return (\n <View style={[styles.container, style]} testID={TURNSTILE_CHALLENGE_TEST_ID}>\n <WebView\n domStorageEnabled\n javaScriptEnabled\n thirdPartyCookiesEnabled\n onError={handleLoadError}\n onHttpError={handleLoadError}\n onMessage={handleMessage}\n // Turnstile renders challenge frames over HTTPS, but its iOS WebView\n // placeholder iframes use about:blank/about:srcdoc (the WebView already\n // allows about:blank by default, so we only need to add about:srcdoc).\n // Keep the whitelist https-only to block cleartext or custom-scheme nav.\n originWhitelist={['https://*', 'about:srcdoc']}\n source={{\n baseUrl,\n html: buildTurnstileHtml(sanitizeSiteKey(siteKey)),\n }}\n style={styles.webView}\n />\n </View>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n // Tall enough for the widget plus the verification animation.\n height: 120,\n overflow: 'hidden',\n },\n webView: {\n backgroundColor: 'transparent',\n flex: 1,\n },\n});\n","import { assertPackageVersion } from '@dynamic-labs-sdk/assert-package-version';\n\nimport {\n name as packageName,\n version as packageVersion,\n} from '../../package.json';\n\nassertPackageVersion(packageName, packageVersion);\n\nexport {\n HCAPTCHA_CHALLENGE_TEST_ID,\n HCaptchaChallenge,\n} from '../components/HCaptchaChallenge/HCaptchaChallenge';\nexport type { HCaptchaChallengeProps } from '../components/HCaptchaChallenge/HCaptchaChallenge';\nexport { HCaptchaError } from '../errors/HCaptchaError';\n\nexport {\n TURNSTILE_CHALLENGE_TEST_ID,\n TurnstileChallenge,\n} from '../components/TurnstileChallenge/TurnstileChallenge';\nexport type { TurnstileChallengeProps } from '../components/TurnstileChallenge/TurnstileChallenge';\nexport { TurnstileError } from '../errors/TurnstileError';\n"],"mappings":"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"}