@usefillo/react 0.16.1 → 0.17.0

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.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode, ReactElement } from 'react';
3
3
  import * as _usefillo_core from '@usefillo/core';
4
- import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloRendererStrings, FilloRespondent, ChallengeConfig, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField, CalculatedField } from '@usefillo/core';
4
+ import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloRendererStrings, FilloRespondent, ChallengeConfig, ChallengeTheme, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField, CalculatedField } from '@usefillo/core';
5
5
  export { CodeForm, Field, FieldValue, FileValue, FilloAppearance, FilloClient, FilloError, FilloJsxError, FilloRendererStrings, FilloSlot, FilloStrings, FormSchema, FormStatus, FormTheme, ProvisionWorkspaceResult, PublishedForm, ResponseData, SlotState, createClient, defineForm, provisionWorkspace, when } from '@usefillo/core';
6
6
 
7
7
  /**
@@ -143,6 +143,18 @@ interface FilloFormBaseProps {
143
143
  * hosted page does this). The SECRET key stays server-side — never passed here.
144
144
  */
145
145
  challenge?: ChallengeConfig;
146
+ /**
147
+ * Theme for the human-verification widget. Defaults to "auto" (the
148
+ * visitor's OS preference); apps with their own theme switch should pass
149
+ * "light"/"dark" so the widget matches the surrounding form.
150
+ */
151
+ challengeTheme?: ChallengeTheme;
152
+ /**
153
+ * Bridge-mode visibility for the human check. "interaction-only" (default)
154
+ * keeps it invisible unless Cloudflare needs the visitor to act; "always"
155
+ * shows the classic widget box the whole time.
156
+ */
157
+ challengeAppearance?: "always" | "interaction-only";
146
158
  /**
147
159
  * Server-owned per-file ceiling when an inline schema skips the normal form
148
160
  * fetch. Fillo's hosted page uses this to preserve its server-rendered fast
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  useId as useId2,
8
8
  useMemo as useMemo6,
9
9
  useRef as useRef9,
10
- useState as useState6,
10
+ useState as useState7,
11
11
  useSyncExternalStore as useSyncExternalStore2
12
12
  } from "react";
13
13
  import {
@@ -2500,10 +2500,12 @@ function FormField({
2500
2500
  }
2501
2501
 
2502
2502
  // src/turnstile.tsx
2503
- import { useEffect as useEffect8, useRef as useRef8 } from "react";
2503
+ import { useEffect as useEffect8, useRef as useRef8, useState as useState6 } from "react";
2504
2504
  import { jsx as jsx7 } from "react/jsx-runtime";
2505
2505
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
2506
2506
  var SCRIPT_ID = "fillo-turnstile-script";
2507
+ var BRIDGE_READY_TIMEOUT_MS = 2e4;
2508
+ var CDATA_RE = /^[A-Za-z0-9_-]{1,255}$/;
2507
2509
  var scriptPromise = null;
2508
2510
  function turnstileGlobal() {
2509
2511
  return globalThis.turnstile;
@@ -2552,8 +2554,133 @@ function loadTurnstileScript() {
2552
2554
  });
2553
2555
  return scriptPromise;
2554
2556
  }
2555
- function TurnstileWidget({
2557
+ function TurnstileWidget(props) {
2558
+ return props.bridgeUrl ? /* @__PURE__ */ jsx7(BridgeChallenge, { ...props, bridgeUrl: props.bridgeUrl }) : /* @__PURE__ */ jsx7(DirectChallenge, { ...props });
2559
+ }
2560
+ function BridgeChallenge({
2561
+ bridgeUrl,
2562
+ formId,
2563
+ theme,
2564
+ appearance,
2565
+ onToken,
2566
+ onError,
2567
+ registerReset,
2568
+ className
2569
+ }) {
2570
+ const iframeRef = useRef8(null);
2571
+ const interactionOnly = (appearance ?? "interaction-only") === "interaction-only";
2572
+ const [frame, setFrame] = useState6(null);
2573
+ const [expanded, setExpanded] = useState6(!interactionOnly);
2574
+ const onTokenRef = useRef8(onToken);
2575
+ onTokenRef.current = onToken;
2576
+ const onErrorRef = useRef8(onError);
2577
+ onErrorRef.current = onError;
2578
+ const registerResetRef = useRef8(registerReset);
2579
+ registerResetRef.current = registerReset;
2580
+ useEffect8(() => {
2581
+ if (typeof window === "undefined") return;
2582
+ let url;
2583
+ try {
2584
+ url = new URL(bridgeUrl);
2585
+ } catch {
2586
+ onErrorRef.current?.();
2587
+ return;
2588
+ }
2589
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
2590
+ onErrorRef.current?.();
2591
+ return;
2592
+ }
2593
+ url.searchParams.set("origin", window.location.origin);
2594
+ url.searchParams.set("theme", theme ?? "auto");
2595
+ url.searchParams.set("appearance", interactionOnly ? "interaction-only" : "always");
2596
+ if (formId && CDATA_RE.test(formId)) url.searchParams.set("cdata", formId);
2597
+ setFrame({ src: url.toString(), origin: url.origin });
2598
+ }, [bridgeUrl, formId, theme, interactionOnly]);
2599
+ useEffect8(() => {
2600
+ if (!frame || typeof window === "undefined") return;
2601
+ let settled = false;
2602
+ const watchdog = window.setTimeout(() => {
2603
+ if (!settled) {
2604
+ onTokenRef.current(void 0);
2605
+ onErrorRef.current?.();
2606
+ }
2607
+ }, BRIDGE_READY_TIMEOUT_MS);
2608
+ const settle = () => {
2609
+ settled = true;
2610
+ window.clearTimeout(watchdog);
2611
+ };
2612
+ const onMessage = (event) => {
2613
+ if (event.origin !== frame.origin) return;
2614
+ const target = iframeRef.current;
2615
+ if (!target || event.source !== target.contentWindow) return;
2616
+ const data = event.data;
2617
+ switch (data?.type) {
2618
+ case "fillo:challenge:ready":
2619
+ settle();
2620
+ break;
2621
+ case "fillo:challenge:token":
2622
+ settle();
2623
+ if (typeof data.token === "string" && data.token.length > 0) {
2624
+ onTokenRef.current(data.token);
2625
+ }
2626
+ if (interactionOnly) setExpanded(false);
2627
+ break;
2628
+ case "fillo:challenge:interactive":
2629
+ if (interactionOnly) setExpanded(true);
2630
+ break;
2631
+ case "fillo:challenge:interactive-done":
2632
+ if (interactionOnly) setExpanded(false);
2633
+ break;
2634
+ case "fillo:challenge:expired":
2635
+ onTokenRef.current(void 0);
2636
+ break;
2637
+ case "fillo:challenge:error":
2638
+ settle();
2639
+ onTokenRef.current(void 0);
2640
+ onErrorRef.current?.();
2641
+ break;
2642
+ }
2643
+ };
2644
+ window.addEventListener("message", onMessage);
2645
+ registerResetRef.current?.(() => {
2646
+ onTokenRef.current(void 0);
2647
+ iframeRef.current?.contentWindow?.postMessage(
2648
+ { type: "fillo:challenge:reset" },
2649
+ frame.origin
2650
+ );
2651
+ });
2652
+ return () => {
2653
+ window.clearTimeout(watchdog);
2654
+ window.removeEventListener("message", onMessage);
2655
+ onTokenRef.current(void 0);
2656
+ };
2657
+ }, [frame, interactionOnly]);
2658
+ return /* @__PURE__ */ jsx7(
2659
+ "div",
2660
+ {
2661
+ className,
2662
+ "data-fillo": "turnstile",
2663
+ "data-fillo-challenge-visible": expanded ? "true" : "false",
2664
+ children: frame && /* @__PURE__ */ jsx7(
2665
+ "iframe",
2666
+ {
2667
+ ref: iframeRef,
2668
+ src: frame.src,
2669
+ title: "Human verification",
2670
+ width: 300,
2671
+ height: expanded ? 65 : 0,
2672
+ className: "fillo-turnstile-frame",
2673
+ referrerPolicy: "no-referrer",
2674
+ "aria-hidden": expanded ? void 0 : true,
2675
+ tabIndex: expanded ? void 0 : -1
2676
+ }
2677
+ )
2678
+ }
2679
+ );
2680
+ }
2681
+ function DirectChallenge({
2556
2682
  siteKey,
2683
+ theme,
2557
2684
  onToken,
2558
2685
  onError,
2559
2686
  registerReset,
@@ -2582,7 +2709,7 @@ function TurnstileWidget({
2582
2709
  };
2583
2710
  widgetIdRef.current = api.render(containerRef.current, {
2584
2711
  sitekey: siteKey,
2585
- theme: "auto",
2712
+ theme: theme ?? "auto",
2586
2713
  callback: (token) => onTokenRef.current(token),
2587
2714
  "error-callback": () => {
2588
2715
  onTokenRef.current(void 0);
@@ -2607,15 +2734,8 @@ function TurnstileWidget({
2607
2734
  widgetIdRef.current = null;
2608
2735
  onTokenRef.current(void 0);
2609
2736
  };
2610
- }, [siteKey]);
2611
- return /* @__PURE__ */ jsx7(
2612
- "div",
2613
- {
2614
- ref: containerRef,
2615
- className,
2616
- "data-fillo": "turnstile"
2617
- }
2618
- );
2737
+ }, [siteKey, theme]);
2738
+ return /* @__PURE__ */ jsx7("div", { ref: containerRef, className, "data-fillo": "turnstile" });
2619
2739
  }
2620
2740
 
2621
2741
  // src/FilloForm.tsx
@@ -2680,8 +2800,8 @@ function FilloForm(props) {
2680
2800
  () => renderOnly ? void 0 : props.client ?? (formId ? createClient() : void 0),
2681
2801
  [renderOnly, props.client, formId]
2682
2802
  );
2683
- const [loaded, setLoaded] = useState6(null);
2684
- const [loadFailure, setLoadFailure] = useState6(
2803
+ const [loaded, setLoaded] = useState7(null);
2804
+ const [loadFailure, setLoadFailure] = useState7(
2685
2805
  null
2686
2806
  );
2687
2807
  const devEnv = useIsDevEnv();
@@ -2996,10 +3116,10 @@ function ResolvedForm(props) {
2996
3116
  const challenge = renderOnly ? void 0 : props.challenge;
2997
3117
  const challengeRequired = Boolean(challenge);
2998
3118
  const challengeTokenRef = useRef9(void 0);
2999
- const [resyncWarning, setResyncWarning] = useState6(null);
3000
- const [resolutionFailureCode, setResolutionFailureCode] = useState6(null);
3001
- const [challengeToken, setChallengeToken] = useState6(void 0);
3002
- const [challengeError, setChallengeError] = useState6(false);
3119
+ const [resyncWarning, setResyncWarning] = useState7(null);
3120
+ const [resolutionFailureCode, setResolutionFailureCode] = useState7(null);
3121
+ const [challengeToken, setChallengeToken] = useState7(void 0);
3122
+ const [challengeError, setChallengeError] = useState7(false);
3003
3123
  const resetChallengeRef = useRef9(void 0);
3004
3124
  const handleChallengeToken = useCallback2((token) => {
3005
3125
  challengeTokenRef.current = token;
@@ -3364,6 +3484,10 @@ function ResolvedForm(props) {
3364
3484
  TurnstileWidget,
3365
3485
  {
3366
3486
  siteKey: challenge.siteKey,
3487
+ bridgeUrl: challenge.bridgeUrl,
3488
+ formId: props.formId,
3489
+ theme: props.challengeTheme,
3490
+ appearance: props.challengeAppearance,
3367
3491
  className: slotClass3("fillo-turnstile", appearance, { slot: "turnstile" }),
3368
3492
  onToken: handleChallengeToken,
3369
3493
  onError: () => setChallengeError(true),
@@ -3531,7 +3655,7 @@ var Fillo = {
3531
3655
  };
3532
3656
 
3533
3657
  // src/provider.tsx
3534
- import { useEffect as useEffect10, useId as useId3, useMemo as useMemo7, useState as useState7 } from "react";
3658
+ import { useEffect as useEffect10, useId as useId3, useMemo as useMemo7, useState as useState8 } from "react";
3535
3659
  import {
3536
3660
  FilloError as FilloError3,
3537
3661
  createClient as createClient2,
@@ -3594,7 +3718,7 @@ function FilloProvider({
3594
3718
  } = useCodeFormSync(form, activeClient, onError, {
3595
3719
  allowUnsynced: Boolean(!activeClient || activeFormId || localPreview)
3596
3720
  });
3597
- const [resyncWarning, setResyncWarning] = useState7(null);
3721
+ const [resyncWarning, setResyncWarning] = useState8(null);
3598
3722
  const rawSchema = codeForm ? resolvedSchema && !devChrome ? resolvedSchema : codeForm.schema : form;
3599
3723
  const normalized = useMemo7(() => normalizeFormSchema2(rawSchema), [rawSchema]);
3600
3724
  if (!normalized.ok) {
package/dist/styles.css CHANGED
@@ -994,6 +994,15 @@
994
994
  .fillo-turnstile {
995
995
  min-height: 65px;
996
996
  }
997
+ /* Bridge mode: the widget lives in Fillo's iframe — keep it flush. */
998
+ .fillo-turnstile-frame {
999
+ display: block;
1000
+ border: 0;
1001
+ }
1002
+ /* interaction-only: while the check runs invisibly, reserve no space. */
1003
+ .fillo-turnstile[data-fillo-challenge-visible="false"] {
1004
+ min-height: 0;
1005
+ }
997
1006
  .fillo-turnstile-error {
998
1007
  margin: 0;
999
1008
  color: var(--fillo-error);
@@ -994,6 +994,15 @@
994
994
  .fillo-turnstile {
995
995
  min-height: 65px;
996
996
  }
997
+ /* Bridge mode: the widget lives in Fillo's iframe — keep it flush. */
998
+ .fillo-turnstile-frame {
999
+ display: block;
1000
+ border: 0;
1001
+ }
1002
+ /* interaction-only: while the check runs invisibly, reserve no space. */
1003
+ .fillo-turnstile[data-fillo-challenge-visible="false"] {
1004
+ min-height: 0;
1005
+ }
997
1006
  .fillo-turnstile-error {
998
1007
  margin: 0;
999
1008
  color: var(--fillo-error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/react",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Headless React components for embedding Fillo forms natively in your product.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@usefillo/core": "^0.16.1"
46
+ "@usefillo/core": "^0.17.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^18.0.0 || ^19.0.0",