@usefillo/core 0.8.0 → 0.9.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 +163 -6
- package/dist/index.js +191 -17
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ interface Condition {
|
|
|
8
8
|
op: ConditionOp;
|
|
9
9
|
value?: string | number | boolean;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A single conditional page-flow rule (P1 logic depth). When every condition in
|
|
13
|
+
* `when` matches (AND — the same evaluator as `visibleIf`), navigation leaves
|
|
14
|
+
* the current page for `to`: another page's id, or the literal `"end"` to finish
|
|
15
|
+
* the form early. An empty `when` is an unconditional jump. Rules are evaluated
|
|
16
|
+
* top-to-bottom; the first match wins. See {@link FormPage.next}.
|
|
17
|
+
*/
|
|
18
|
+
interface JumpRule {
|
|
19
|
+
when: Condition[];
|
|
20
|
+
/** Target page id, or "end" to finish the form. */
|
|
21
|
+
to: string | "end";
|
|
22
|
+
}
|
|
11
23
|
type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "custom";
|
|
12
24
|
type ContentKind = "heading" | "paragraph" | "divider";
|
|
13
25
|
type BlockKind = FieldKind | ContentKind;
|
|
@@ -151,6 +163,10 @@ interface FormPage {
|
|
|
151
163
|
id: string;
|
|
152
164
|
title?: string;
|
|
153
165
|
blocks: Block[];
|
|
166
|
+
/** Conditional page flow (P1 logic depth). Rules are evaluated top-to-bottom;
|
|
167
|
+
* the first whose conditions all match decides the next step. No rule matches
|
|
168
|
+
* → default linear next page. Absent → today's linear behavior. */
|
|
169
|
+
next?: JumpRule[];
|
|
154
170
|
}
|
|
155
171
|
/**
|
|
156
172
|
* Limit repeat responses. Absent = no limit (submit as often as you like).
|
|
@@ -185,6 +201,22 @@ interface ResponseLimit {
|
|
|
185
201
|
*/
|
|
186
202
|
onRepeat: "keep" | "update";
|
|
187
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Per-form submission-trust policy. Absent = accept everything (today's
|
|
206
|
+
* behavior). See docs/roadmap/07-submission-trust.md.
|
|
207
|
+
*/
|
|
208
|
+
interface TrustPolicy {
|
|
209
|
+
/** What to do with a submission whose respondent is NOT HMAC-verified.
|
|
210
|
+
* "allow" (default) accepts it normally; "quarantine" stores it withheld
|
|
211
|
+
* from every downstream consumer until an owner releases it. Declared-agent
|
|
212
|
+
* classes come in a later phase. */
|
|
213
|
+
unverified?: "allow" | "quarantine";
|
|
214
|
+
/** Require a human-verification challenge before accepting a submission.
|
|
215
|
+
* "off" (default) = no challenge; "turnstile" = a Cloudflare Turnstile widget
|
|
216
|
+
* that the SDK renders and the server verifies. Provider-agnostic on purpose.
|
|
217
|
+
* Independent of `unverified` — a form can require both. */
|
|
218
|
+
challenge?: "off" | "turnstile";
|
|
219
|
+
}
|
|
188
220
|
interface FormSettings {
|
|
189
221
|
/**
|
|
190
222
|
* Default "button": respondents submit with the footer button. "auto" hides
|
|
@@ -200,6 +232,9 @@ interface FormSettings {
|
|
|
200
232
|
/** Limit repeat responses (who counts as the same responder, and what a
|
|
201
233
|
* repeat does). Absent = no limit. See {@link ResponseLimit}. */
|
|
202
234
|
responseLimit?: ResponseLimit;
|
|
235
|
+
/** Per-form submission-trust policy. Absent = accept everything (today's
|
|
236
|
+
* behavior). See docs/roadmap/07-submission-trust.md. */
|
|
237
|
+
trust?: TrustPolicy;
|
|
203
238
|
/** Notify this address on every submission. */
|
|
204
239
|
notifyEmail?: string;
|
|
205
240
|
/** Send respondents a receipt (to the first answered email field). */
|
|
@@ -351,6 +386,13 @@ interface UploadSession {
|
|
|
351
386
|
file?: FileValue;
|
|
352
387
|
}
|
|
353
388
|
|
|
389
|
+
/**
|
|
390
|
+
* All conditions must hold (AND); no conditions = true. THE shared evaluator —
|
|
391
|
+
* both block visibility (`visibleIf`) and page jumps (`JumpRule.when`) run
|
|
392
|
+
* through this over the SAME whole-form fixpoint resolver, so a jump gated by a
|
|
393
|
+
* logic-hidden controller field behaves identically to a visibility rule.
|
|
394
|
+
*/
|
|
395
|
+
declare function conditionsMet(conds: Condition[], resolve: (fieldId: string) => FieldValue): boolean;
|
|
354
396
|
/** All conditions must hold (AND). No conditions = visible. */
|
|
355
397
|
declare function isBlockVisible(block: Block, data: ResponseData): boolean;
|
|
356
398
|
declare function visibleBlocks(page: FormPage, data: ResponseData): Block[];
|
|
@@ -378,8 +420,57 @@ declare function allFields(form: FormSchema): Field[];
|
|
|
378
420
|
* per-visitor key and the server's per-person dedup so both scope identically.
|
|
379
421
|
*/
|
|
380
422
|
declare function responseScopeValue(settings: FormSettings, data: ResponseData): string | null;
|
|
381
|
-
/** Fields currently visible given the response data
|
|
423
|
+
/** Fields currently visible given the response data. */
|
|
382
424
|
declare function visibleFields(form: FormSchema, data: ResponseData): Field[];
|
|
425
|
+
/** Where navigation goes when leaving a page. */
|
|
426
|
+
type NextPage = {
|
|
427
|
+
to: string;
|
|
428
|
+
} | {
|
|
429
|
+
end: true;
|
|
430
|
+
} | {
|
|
431
|
+
linear: true;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Evaluate a page's `next` jump rules top-to-bottom against the current data;
|
|
435
|
+
* the first whose conditions all match decides the step ("end" → finish the
|
|
436
|
+
* form). No rule (or no `next`) → the default linear next page. THE single
|
|
437
|
+
* function the client renderer, the server validator, and the funnel share so
|
|
438
|
+
* they always agree on flow.
|
|
439
|
+
*/
|
|
440
|
+
declare function resolveNextPage(form: FormSchema, currentPageId: string, data: ResponseData): NextPage;
|
|
441
|
+
/**
|
|
442
|
+
* The ORDERED list of reachable page ids for the given data: walk from
|
|
443
|
+
* `pages[0]` following `resolveNextPage` (linear → next index; jump → target id;
|
|
444
|
+
* end → stop). A rule that points backward could loop, so stop on the first
|
|
445
|
+
* revisit and cap the walk at `pages.length` steps. THE single ordered engine
|
|
446
|
+
* the client renderer, the server validator, and navigation all agree through —
|
|
447
|
+
* a no-jump form yields `[pages[0].id, …, pages[N].id]`, so everything built on
|
|
448
|
+
* it reduces to today's linear behavior. {@link reachablePageIds} is the set of
|
|
449
|
+
* this exact walk (one walk, no divergence).
|
|
450
|
+
*/
|
|
451
|
+
declare function reachablePageSequence(form: FormSchema, data: ResponseData): string[];
|
|
452
|
+
/**
|
|
453
|
+
* The page ids reachable for the given data. The unordered set of
|
|
454
|
+
* {@link reachablePageSequence} — one shared walk keeps the ordered navigation
|
|
455
|
+
* and the reachability the validator uses provably in agreement. THE function
|
|
456
|
+
* the client renderer and server validator both use to decide which pages/fields
|
|
457
|
+
* are in play.
|
|
458
|
+
*/
|
|
459
|
+
declare function reachablePageIds(form: FormSchema, data: ResponseData): Set<string>;
|
|
460
|
+
/**
|
|
461
|
+
* Whether `pageId` is terminal for the given data — pressing the footer button
|
|
462
|
+
* there submits rather than advancing. True when a matched jump rule resolves to
|
|
463
|
+
* "end", or when the page is the LAST element of the reachable sequence (the
|
|
464
|
+
* last reachable page, including a cycle broken at its revisit, so a backward
|
|
465
|
+
* jump can never loop — the pre-revisit page becomes terminal and Submit
|
|
466
|
+
* appears). For a no-jump form this is exactly "the last page".
|
|
467
|
+
*/
|
|
468
|
+
declare function isTerminalPage(form: FormSchema, pageId: string, data: ResponseData): boolean;
|
|
469
|
+
/** Reachable field ids: fields on reachable pages, intersected with visibility. */
|
|
470
|
+
declare function reachableFieldIds(form: FormSchema, data: ResponseData): Set<string>;
|
|
471
|
+
/** Fields that are both reachable AND visible — the set that gets validated and
|
|
472
|
+
* kept on submit. Equals {@link visibleFields} for a form with no jumps. */
|
|
473
|
+
declare function reachableFields(form: FormSchema, data: ResponseData): Field[];
|
|
383
474
|
|
|
384
475
|
/** Validate a single answered value for a field. Returns an error message or null. */
|
|
385
476
|
declare function validateField(field: Field, value: FieldValue): string | null;
|
|
@@ -387,12 +478,18 @@ interface ValidationResult {
|
|
|
387
478
|
ok: boolean;
|
|
388
479
|
/** fieldId -> message for every failing field. */
|
|
389
480
|
errors: Record<string, string>;
|
|
390
|
-
/** Data trimmed to the fields that are visible (
|
|
481
|
+
/** Data trimmed to the fields that are reachable + visible (answers to fields
|
|
482
|
+
* hidden by logic OR on pages skipped by a jump/early-end are dropped). */
|
|
391
483
|
data: ResponseData;
|
|
392
484
|
}
|
|
393
485
|
/**
|
|
394
|
-
* Validate a full submission against the schema. Only currently-
|
|
395
|
-
* are validated and kept — answers to fields hidden by logic
|
|
486
|
+
* Validate a full submission against the schema. Only reachable + currently-
|
|
487
|
+
* visible fields are validated and kept — answers to fields hidden by logic, or
|
|
488
|
+
* on pages a page-jump/early-end skipped, are discarded. A legitimately
|
|
489
|
+
* early-ended submission therefore does NOT 422 on a skipped page's required
|
|
490
|
+
* field. Reachability is computed by the SAME shared engine the client renderer
|
|
491
|
+
* navigates with, so render and validate always agree. A form with no jumps has
|
|
492
|
+
* every page reachable, so this validates exactly as before.
|
|
396
493
|
*/
|
|
397
494
|
declare function validateResponse(form: FormSchema, data: ResponseData): ValidationResult;
|
|
398
495
|
|
|
@@ -409,6 +506,14 @@ declare const FILLO_SDK_VERSION: string;
|
|
|
409
506
|
* breaks are gated separately by FILLO_SCHEMA_VERSION, so this floor stays low.
|
|
410
507
|
*/
|
|
411
508
|
declare const FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
509
|
+
/**
|
|
510
|
+
* The floor served INSTEAD of FILLO_MIN_SDK_VERSION for challenge-enabled
|
|
511
|
+
* forms: the first release that ships the Turnstile widget. An older SDK passes
|
|
512
|
+
* the base floor but renders no widget, so the server would reject its every
|
|
513
|
+
* submit — the raised floor makes it fail fast with the clear "update
|
|
514
|
+
* @usefillo/*" error instead of a form that silently can't submit.
|
|
515
|
+
*/
|
|
516
|
+
declare const FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
412
517
|
declare function normalizeSettings(value: unknown): FormSettings;
|
|
413
518
|
interface SchemaValidationResult {
|
|
414
519
|
ok: boolean;
|
|
@@ -578,6 +683,18 @@ interface FilloClientOptions {
|
|
|
578
683
|
baseUrl?: string;
|
|
579
684
|
fetch?: typeof fetch;
|
|
580
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Public human-verification challenge config the SDK needs to render a widget.
|
|
688
|
+
* Delivered as a TOP-LEVEL field on the form GET (never inside the schema): the
|
|
689
|
+
* schema's trust policy is server-only and stripped, but the widget needs the
|
|
690
|
+
* PUBLIC site key. Injected server-side from Fillo's env — the SECRET key never
|
|
691
|
+
* leaves the server. Absent = no challenge (render nothing, load no script).
|
|
692
|
+
*/
|
|
693
|
+
interface ChallengeConfig {
|
|
694
|
+
provider: "turnstile";
|
|
695
|
+
/** Cloudflare Turnstile PUBLIC site key. Safe to ship to the browser. */
|
|
696
|
+
siteKey: string;
|
|
697
|
+
}
|
|
581
698
|
interface PublishedForm {
|
|
582
699
|
id: string;
|
|
583
700
|
slug: string;
|
|
@@ -593,6 +710,9 @@ interface PublishedForm {
|
|
|
593
710
|
closed?: boolean;
|
|
594
711
|
/** Workspace branding state — absent means show the badge (default). */
|
|
595
712
|
branding?: FormBranding;
|
|
713
|
+
/** Human-verification challenge to render before submit, when the form
|
|
714
|
+
* requires one. Absent = no challenge. Carries only the PUBLIC site key. */
|
|
715
|
+
challenge?: ChallengeConfig;
|
|
596
716
|
}
|
|
597
717
|
type SubmitResult = {
|
|
598
718
|
ok: true;
|
|
@@ -638,6 +758,13 @@ interface SubmitMeta {
|
|
|
638
758
|
* stable user/account id.
|
|
639
759
|
*/
|
|
640
760
|
respondent?: FilloRespondent;
|
|
761
|
+
/**
|
|
762
|
+
* Human-verification challenge token (e.g. from the Cloudflare Turnstile
|
|
763
|
+
* widget). Present only when the form requires a challenge; the server
|
|
764
|
+
* verifies it and rejects the submit if it is missing or invalid. Never a
|
|
765
|
+
* secret — it is a single-use, server-verifiable proof of the widget solve.
|
|
766
|
+
*/
|
|
767
|
+
challengeToken?: string;
|
|
641
768
|
}
|
|
642
769
|
/**
|
|
643
770
|
* The host app's account context for the person filling the form. Passed as
|
|
@@ -726,6 +853,10 @@ interface SyncFormResult {
|
|
|
726
853
|
formId: string;
|
|
727
854
|
slug: string;
|
|
728
855
|
branding?: FormBranding;
|
|
856
|
+
/** Human-verification challenge to render before submit, when the LIVE form
|
|
857
|
+
* requires one (staged changes don't gate until published). Absent = no
|
|
858
|
+
* challenge. Carries only the PUBLIC site key. */
|
|
859
|
+
challenge?: ChallengeConfig;
|
|
729
860
|
/** Lifecycle on newer servers: a draft can't accept public responses yet. */
|
|
730
861
|
status?: "draft" | "published";
|
|
731
862
|
/** Changes were staged as a draft for a human to publish. */
|
|
@@ -946,6 +1077,24 @@ interface FormControllerOptions {
|
|
|
946
1077
|
* the respondent can do.
|
|
947
1078
|
*/
|
|
948
1079
|
respondent?: FilloRespondent;
|
|
1080
|
+
/**
|
|
1081
|
+
* True when the form requires a human-verification challenge (Turnstile).
|
|
1082
|
+
* When set, submit refuses to send until a token is available and always
|
|
1083
|
+
* attaches the token from {@link getChallengeToken}. The server is the real
|
|
1084
|
+
* gate — this only avoids firing a submit the server would reject.
|
|
1085
|
+
*/
|
|
1086
|
+
challengeRequired?: boolean;
|
|
1087
|
+
/**
|
|
1088
|
+
* Read the current challenge token (from the rendered widget) at submit time.
|
|
1089
|
+
* Returns undefined until the challenge is solved. Read lazily so an expired
|
|
1090
|
+
* token that was refreshed just before submit is picked up fresh.
|
|
1091
|
+
*/
|
|
1092
|
+
getChallengeToken?: () => string | undefined;
|
|
1093
|
+
/**
|
|
1094
|
+
* The server rejected the submission's challenge (stale/replayed/invalid
|
|
1095
|
+
* token). The renderer resets the widget so the human can solve a fresh one.
|
|
1096
|
+
*/
|
|
1097
|
+
onChallengeFailed?: () => void;
|
|
949
1098
|
}
|
|
950
1099
|
interface FormControllerState {
|
|
951
1100
|
data: ResponseData;
|
|
@@ -1064,6 +1213,8 @@ interface AutoSubmitContext {
|
|
|
1064
1213
|
form: FormSchema;
|
|
1065
1214
|
data: ResponseData;
|
|
1066
1215
|
status: FormStatus;
|
|
1216
|
+
/** Terminal-aware last-page flag from the controller. Advisory here —
|
|
1217
|
+
* shouldAutoSubmit re-derives terminal from the RESULTING data (see below). */
|
|
1067
1218
|
isLastPage: boolean;
|
|
1068
1219
|
uploading: boolean;
|
|
1069
1220
|
}
|
|
@@ -1075,7 +1226,7 @@ declare function shouldAutoSubmit(field: Field, value: FieldValue, ctx: AutoSubm
|
|
|
1075
1226
|
* (Tailwind `data-[invalid]:…`) can react to it. Slot names and attribute
|
|
1076
1227
|
* names are public API — renames are breaking; additions are minors.
|
|
1077
1228
|
*/
|
|
1078
|
-
declare const FILLO_SLOTS: readonly ["root", "header", "title", "description", "pageTitle", "progress", "progressFill", "blocks", "field", "label", "fieldDescription", "control", "options", "option", "optionLabel", "error", "footer", "button", "success", "resume"];
|
|
1229
|
+
declare const FILLO_SLOTS: readonly ["root", "header", "title", "description", "pageTitle", "progress", "progressFill", "blocks", "field", "label", "fieldDescription", "control", "options", "option", "optionLabel", "error", "footer", "button", "success", "resume", "turnstile"];
|
|
1079
1230
|
type FilloSlot = (typeof FILLO_SLOTS)[number];
|
|
1080
1231
|
/** data-* names emitted alongside the stable fillo-* classes. */
|
|
1081
1232
|
declare const FILLO_DATA_ATTRS: {
|
|
@@ -1211,6 +1362,8 @@ interface FilloStrings {
|
|
|
1211
1362
|
editNotice: string;
|
|
1212
1363
|
/** The discard action next to the resume notice. */
|
|
1213
1364
|
resumeStartOver: string;
|
|
1365
|
+
/** Shown when the human-verification widget can't load (script blocked). */
|
|
1366
|
+
challengeUnavailable: string;
|
|
1214
1367
|
}
|
|
1215
1368
|
declare const DEFAULT_STRINGS: FilloStrings;
|
|
1216
1369
|
/**
|
|
@@ -1258,6 +1411,10 @@ interface SyncedForm {
|
|
|
1258
1411
|
formId: string;
|
|
1259
1412
|
slug: string;
|
|
1260
1413
|
branding?: FormBranding;
|
|
1414
|
+
/** Human-verification challenge to render before submit, when the LIVE form
|
|
1415
|
+
* requires one (staged changes don't gate until published). Absent = no
|
|
1416
|
+
* challenge. Carries only the PUBLIC site key. */
|
|
1417
|
+
challenge?: ChallengeConfig;
|
|
1261
1418
|
/** Lifecycle on newer servers: a draft can't accept public responses yet. */
|
|
1262
1419
|
status?: "draft" | "published";
|
|
1263
1420
|
/** Changes were staged as a draft for a human to publish. */
|
|
@@ -1430,4 +1587,4 @@ declare class Sha1 {
|
|
|
1430
1587
|
}
|
|
1431
1588
|
declare const sha1Base64: (bytes: Uint8Array) => string;
|
|
1432
1589
|
|
|
1433
|
-
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type LinearScaleField, type MatrixField, type NumberField, PHONE_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, flagEmoji, formSchemasEqual, formatAnswer, formatNational, isAutoSubmitBlock, isBlockVisible, isCodeForm, isField, isFilloError, isPossiblePhone, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, resolveSlotClass, resolveStrings, resolveText, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
|
1590
|
+
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, flagEmoji, formSchemasEqual, formatAnswer, formatNational, isAutoSubmitBlock, isBlockVisible, isCodeForm, isField, isFilloError, isPossiblePhone, isTerminalPage, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
package/dist/index.js
CHANGED
|
@@ -54,9 +54,13 @@ function evalCondition(cond, resolve) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
function conditionsMet(conds, resolve) {
|
|
58
|
+
if (conds.length === 0) return true;
|
|
59
|
+
return conds.every((cond) => evalCondition(cond, resolve));
|
|
60
|
+
}
|
|
57
61
|
function blockVisibleWith(block, resolve) {
|
|
58
62
|
if (!block.visibleIf || block.visibleIf.length === 0) return true;
|
|
59
|
-
return block.visibleIf
|
|
63
|
+
return conditionsMet(block.visibleIf, resolve);
|
|
60
64
|
}
|
|
61
65
|
function makeResolver(data, scoped, visible) {
|
|
62
66
|
return (fieldId) => scoped.has(fieldId) && !visible.has(fieldId) ? void 0 : data[fieldId];
|
|
@@ -108,6 +112,70 @@ function visibleFields(form, data) {
|
|
|
108
112
|
const visible = visibleFieldIds(fields, data);
|
|
109
113
|
return fields.filter((f) => visible.has(f.id));
|
|
110
114
|
}
|
|
115
|
+
function formResolver(form, data) {
|
|
116
|
+
const fields = allFields(form);
|
|
117
|
+
const scoped = new Set(fields.map((f) => f.id));
|
|
118
|
+
return makeResolver(data, scoped, visibleFieldIds(fields, data));
|
|
119
|
+
}
|
|
120
|
+
function resolveNextPage(form, currentPageId, data) {
|
|
121
|
+
const page = form.pages.find((p) => p.id === currentPageId);
|
|
122
|
+
const rules = page?.next;
|
|
123
|
+
if (!rules || rules.length === 0) return { linear: true };
|
|
124
|
+
const resolve = formResolver(form, data);
|
|
125
|
+
for (const rule of rules) {
|
|
126
|
+
if (conditionsMet(rule.when, resolve)) {
|
|
127
|
+
return rule.to === "end" ? { end: true } : { to: rule.to };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { linear: true };
|
|
131
|
+
}
|
|
132
|
+
function reachablePageSequence(form, data) {
|
|
133
|
+
const seq = [];
|
|
134
|
+
if (form.pages.length === 0) return seq;
|
|
135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
136
|
+
let index = 0;
|
|
137
|
+
for (let steps = 0; steps <= form.pages.length; steps++) {
|
|
138
|
+
const page = form.pages[index];
|
|
139
|
+
if (!page || seen.has(page.id)) break;
|
|
140
|
+
seen.add(page.id);
|
|
141
|
+
seq.push(page.id);
|
|
142
|
+
const nav = resolveNextPage(form, page.id, data);
|
|
143
|
+
if ("end" in nav) break;
|
|
144
|
+
if ("to" in nav) {
|
|
145
|
+
const target = form.pages.findIndex((p) => p.id === nav.to);
|
|
146
|
+
if (target < 0) break;
|
|
147
|
+
index = target;
|
|
148
|
+
} else {
|
|
149
|
+
index += 1;
|
|
150
|
+
if (index >= form.pages.length) break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return seq;
|
|
154
|
+
}
|
|
155
|
+
function reachablePageIds(form, data) {
|
|
156
|
+
return new Set(reachablePageSequence(form, data));
|
|
157
|
+
}
|
|
158
|
+
function isTerminalPage(form, pageId, data) {
|
|
159
|
+
if ("end" in resolveNextPage(form, pageId, data)) return true;
|
|
160
|
+
const seq = reachablePageSequence(form, data);
|
|
161
|
+
return seq.length > 0 && seq[seq.length - 1] === pageId;
|
|
162
|
+
}
|
|
163
|
+
function reachableFieldIds(form, data) {
|
|
164
|
+
const reachablePages = reachablePageIds(form, data);
|
|
165
|
+
const visible = visibleFieldIds(allFields(form), data);
|
|
166
|
+
const ids = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const page of form.pages) {
|
|
168
|
+
if (!reachablePages.has(page.id)) continue;
|
|
169
|
+
for (const block of page.blocks) {
|
|
170
|
+
if (isField(block) && visible.has(block.id)) ids.add(block.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return ids;
|
|
174
|
+
}
|
|
175
|
+
function reachableFields(form, data) {
|
|
176
|
+
const ids = reachableFieldIds(form, data);
|
|
177
|
+
return allFields(form).filter((f) => ids.has(f.id));
|
|
178
|
+
}
|
|
111
179
|
|
|
112
180
|
// src/validation.ts
|
|
113
181
|
import * as z from "zod/mini";
|
|
@@ -440,7 +508,8 @@ var DEFAULT_STRINGS = {
|
|
|
440
508
|
renderFailed: "This form could not be rendered.",
|
|
441
509
|
resumeNotice: "Picked up where you left off.",
|
|
442
510
|
editNotice: "You're updating your earlier response.",
|
|
443
|
-
resumeStartOver: "Start over"
|
|
511
|
+
resumeStartOver: "Start over",
|
|
512
|
+
challengeUnavailable: "The verification check couldn't load. Refresh the page, or check that challenges.cloudflare.com isn't blocked."
|
|
444
513
|
};
|
|
445
514
|
var DEFAULT_FIELD_STRINGS = {
|
|
446
515
|
required: REQUIRED_FIELD_MESSAGE,
|
|
@@ -617,7 +686,7 @@ function validateField(field2, value) {
|
|
|
617
686
|
}
|
|
618
687
|
}
|
|
619
688
|
function validateResponse(form, data) {
|
|
620
|
-
const fields =
|
|
689
|
+
const fields = reachableFields(form, data);
|
|
621
690
|
const errors = {};
|
|
622
691
|
const cleaned = {};
|
|
623
692
|
for (const field2 of fields) {
|
|
@@ -688,7 +757,11 @@ var blockSchema = z2.looseObject({
|
|
|
688
757
|
var pageSchema = z2.object({
|
|
689
758
|
id: idSchema,
|
|
690
759
|
title: z2.optional(z2.string().check(z2.maxLength(500))),
|
|
691
|
-
blocks: z2.array(blockSchema).check(z2.maxLength(500))
|
|
760
|
+
blocks: z2.array(blockSchema).check(z2.maxLength(500)),
|
|
761
|
+
// Optional conditional page flow. Kept as unknown here (this schema is strict,
|
|
762
|
+
// so it would otherwise be dropped at parse) and normalized by hand below once
|
|
763
|
+
// the page-id set is known — a jump to a missing page is dropped, never kept.
|
|
764
|
+
next: z2.optional(z2.unknown())
|
|
692
765
|
});
|
|
693
766
|
var schemaShape = z2.object({
|
|
694
767
|
version: z2.literal(1),
|
|
@@ -699,8 +772,9 @@ var schemaShape = z2.object({
|
|
|
699
772
|
});
|
|
700
773
|
var MAX_SCHEMA_VERSION = 1;
|
|
701
774
|
var FILLO_SCHEMA_VERSION = 1;
|
|
702
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
775
|
+
var FILLO_SDK_VERSION = true ? "0.9.0" : "0.0.0-dev";
|
|
703
776
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
777
|
+
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
704
778
|
function str(value, max, fallback = "") {
|
|
705
779
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
706
780
|
}
|
|
@@ -755,6 +829,23 @@ function conditions(value) {
|
|
|
755
829
|
});
|
|
756
830
|
return normalized.length ? normalized : void 0;
|
|
757
831
|
}
|
|
832
|
+
function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
|
|
833
|
+
if (!Array.isArray(value)) return void 0;
|
|
834
|
+
const rules = value.slice(0, 50).flatMap((item) => {
|
|
835
|
+
if (!item || typeof item !== "object") return [];
|
|
836
|
+
const rec = item;
|
|
837
|
+
const to = typeof rec.to === "string" ? rec.to : void 0;
|
|
838
|
+
if (!to || to !== "end" && !pageIds.has(to)) return [];
|
|
839
|
+
if (to === sourceId) return [];
|
|
840
|
+
const rawWhen = rec.when;
|
|
841
|
+
if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
|
|
842
|
+
const when2 = conditions(rawWhen);
|
|
843
|
+
if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
|
|
844
|
+
if (when2 && when2.some((c) => !allowedFieldIds.has(c.fieldId))) return [];
|
|
845
|
+
return [{ when: when2 ?? [], to }];
|
|
846
|
+
});
|
|
847
|
+
return rules.length ? rules : void 0;
|
|
848
|
+
}
|
|
758
849
|
function baseBlock(rec) {
|
|
759
850
|
return {
|
|
760
851
|
id: str(rec.id, 128),
|
|
@@ -798,6 +889,17 @@ function normalizeResponseLimit(value) {
|
|
|
798
889
|
onRepeat
|
|
799
890
|
};
|
|
800
891
|
}
|
|
892
|
+
function normalizeTrust(value) {
|
|
893
|
+
if (!value || typeof value !== "object") return void 0;
|
|
894
|
+
const rec = value;
|
|
895
|
+
const unverified = rec.unverified === "allow" || rec.unverified === "quarantine" ? rec.unverified : void 0;
|
|
896
|
+
const challenge = rec.challenge === "turnstile" ? "turnstile" : void 0;
|
|
897
|
+
if (!unverified && !challenge) return void 0;
|
|
898
|
+
return {
|
|
899
|
+
...unverified ? { unverified } : {},
|
|
900
|
+
...challenge ? { challenge } : {}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
801
903
|
function normalizeSettings(value) {
|
|
802
904
|
const rec = value && typeof value === "object" ? value : {};
|
|
803
905
|
const submitMode = rec.submitMode === "button" || rec.submitMode === "auto" ? rec.submitMode : void 0;
|
|
@@ -809,6 +911,7 @@ function normalizeSettings(value) {
|
|
|
809
911
|
redirectUrl: normalizeUrl(rec.redirectUrl),
|
|
810
912
|
showProgress: bool(rec.showProgress),
|
|
811
913
|
responseLimit: normalizeResponseLimit(rec.responseLimit),
|
|
914
|
+
trust: normalizeTrust(rec.trust),
|
|
812
915
|
notifyEmail: z2.email().check(z2.maxLength(254)).safeParse(rec.notifyEmail).success ? rec.notifyEmail : void 0,
|
|
813
916
|
sendReceipt: bool(rec.sendReceipt),
|
|
814
917
|
saveProgress: bool(rec.saveProgress),
|
|
@@ -948,15 +1051,24 @@ function normalizeFormSchema(input) {
|
|
|
948
1051
|
if (parsed.data.version > MAX_SCHEMA_VERSION) {
|
|
949
1052
|
return { ok: false, error: `Unsupported schema version: ${parsed.data.version}` };
|
|
950
1053
|
}
|
|
951
|
-
const
|
|
1054
|
+
const rebuilt = parsed.data.pages.map((page) => {
|
|
952
1055
|
const blocks = page.blocks.flatMap((raw) => {
|
|
953
1056
|
const block = normalizeBlock(raw);
|
|
954
1057
|
return block ? [block] : [];
|
|
955
1058
|
});
|
|
1059
|
+
return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
|
|
1060
|
+
});
|
|
1061
|
+
const targetIds = new Set(rebuilt.map((p) => p.id));
|
|
1062
|
+
const priorFieldIds = /* @__PURE__ */ new Set();
|
|
1063
|
+
const pages = rebuilt.map((page) => {
|
|
1064
|
+
for (const block of page.blocks) {
|
|
1065
|
+
if (isField(block)) priorFieldIds.add(block.id);
|
|
1066
|
+
}
|
|
956
1067
|
return cleanObject({
|
|
957
|
-
id:
|
|
958
|
-
title:
|
|
959
|
-
blocks
|
|
1068
|
+
id: page.id,
|
|
1069
|
+
title: page.title,
|
|
1070
|
+
blocks: page.blocks,
|
|
1071
|
+
next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
|
|
960
1072
|
});
|
|
961
1073
|
});
|
|
962
1074
|
const pageIds = /* @__PURE__ */ new Set();
|
|
@@ -2353,6 +2465,8 @@ function isDraftGone(err) {
|
|
|
2353
2465
|
return isFilloError(err) && (err.status === 401 || err.status === 403 || err.status === 404);
|
|
2354
2466
|
}
|
|
2355
2467
|
var DRAFT_DEBOUNCE_MS = 1500;
|
|
2468
|
+
var CHALLENGE_INCOMPLETE_MESSAGE = "Please complete the verification check, then submit.";
|
|
2469
|
+
var CHALLENGE_RETRY_MESSAGE = "That verification didn't go through. Please complete the check again and resubmit.";
|
|
2356
2470
|
function submitFailureMessage(err) {
|
|
2357
2471
|
if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
|
|
2358
2472
|
return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
|
|
@@ -2458,7 +2572,10 @@ function createFormController(options2) {
|
|
|
2458
2572
|
// keeps it — never a field whose answer submit would silently drop.
|
|
2459
2573
|
blocks: visiblePageBlocks(form, page, data),
|
|
2460
2574
|
isFirstPage: clamped === 0,
|
|
2461
|
-
|
|
2575
|
+
// Terminal-aware: true when the current page is the last reachable page OR
|
|
2576
|
+
// a matched jump rule ends the form here — so the footer reads Submit and
|
|
2577
|
+
// its handler submits. Same shared engine the validator reaches with.
|
|
2578
|
+
isLastPage: isTerminalPage(form, page.id, data),
|
|
2462
2579
|
uploading: uploadingFields.size > 0,
|
|
2463
2580
|
submitError,
|
|
2464
2581
|
restoredSubmission,
|
|
@@ -2659,6 +2776,20 @@ function createFormController(options2) {
|
|
|
2659
2776
|
else uploadingFields.delete(fieldId);
|
|
2660
2777
|
notify();
|
|
2661
2778
|
}
|
|
2779
|
+
function currentSeqPosition(seq) {
|
|
2780
|
+
if (seq.length === 0) return -1;
|
|
2781
|
+
const pageCount = form.pages.length;
|
|
2782
|
+
const clamped = Math.min(pageIndex, Math.max(pageCount - 1, 0));
|
|
2783
|
+
const currentId = form.pages[clamped]?.id;
|
|
2784
|
+
const exact = currentId === void 0 ? -1 : seq.indexOf(currentId);
|
|
2785
|
+
if (exact >= 0) return exact;
|
|
2786
|
+
let fallback = 0;
|
|
2787
|
+
for (let i = 0; i < seq.length; i++) {
|
|
2788
|
+
const idx = form.pages.findIndex((p) => p.id === seq[i]);
|
|
2789
|
+
if (idx >= 0 && idx <= clamped) fallback = i;
|
|
2790
|
+
}
|
|
2791
|
+
return fallback;
|
|
2792
|
+
}
|
|
2662
2793
|
function next() {
|
|
2663
2794
|
const pageCount = form.pages.length;
|
|
2664
2795
|
const clamped = Math.min(pageIndex, Math.max(pageCount - 1, 0));
|
|
@@ -2670,14 +2801,31 @@ function createFormController(options2) {
|
|
|
2670
2801
|
return;
|
|
2671
2802
|
}
|
|
2672
2803
|
}
|
|
2673
|
-
|
|
2804
|
+
const current = form.pages[clamped];
|
|
2805
|
+
const seq = reachablePageSequence(form, data);
|
|
2806
|
+
const pos = currentSeqPosition(seq);
|
|
2807
|
+
const terminal = (current ? isTerminalPage(form, current.id, data) : true) || pos < 0 || pos + 1 >= seq.length;
|
|
2808
|
+
if (terminal) {
|
|
2809
|
+
void submit();
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const nextIndex = form.pages.findIndex((p) => p.id === seq[pos + 1]);
|
|
2813
|
+
pageIndex = nextIndex >= 0 ? nextIndex : Math.min(clamped + 1, pageCount - 1);
|
|
2674
2814
|
if (sessionId && client) client.reportProgress(sessionId, { furthestPage: pageIndex });
|
|
2675
2815
|
notify();
|
|
2676
2816
|
checkpointDraft();
|
|
2677
2817
|
}
|
|
2678
2818
|
function back() {
|
|
2679
2819
|
errors = {};
|
|
2680
|
-
|
|
2820
|
+
const seq = reachablePageSequence(form, data);
|
|
2821
|
+
const pos = currentSeqPosition(seq);
|
|
2822
|
+
if (pos > 0) {
|
|
2823
|
+
const prevIndex = form.pages.findIndex((p) => p.id === seq[pos - 1]);
|
|
2824
|
+
pageIndex = prevIndex >= 0 ? prevIndex : Math.max(Math.min(pageIndex, form.pages.length - 1) - 1, 0);
|
|
2825
|
+
} else if (seq.length > 0) {
|
|
2826
|
+
const firstIndex = form.pages.findIndex((p) => p.id === seq[0]);
|
|
2827
|
+
pageIndex = firstIndex >= 0 ? firstIndex : 0;
|
|
2828
|
+
}
|
|
2681
2829
|
notify();
|
|
2682
2830
|
checkpointDraft();
|
|
2683
2831
|
}
|
|
@@ -2710,6 +2858,13 @@ function createFormController(options2) {
|
|
|
2710
2858
|
const visitorKey = form.settings.responseLimit?.by === "browser" ? ensureSubmissionKey(visitorSubmissionKeyId(form, formId, result.data)) : void 0;
|
|
2711
2859
|
const submissionKey = visitorKey ?? idempotencyKey;
|
|
2712
2860
|
const submitDraft = form.settings.saveProgress ? draftRef ?? readDraftRef(formId) : null;
|
|
2861
|
+
const challengeToken = options2.challengeRequired ? options2.getChallengeToken?.() : void 0;
|
|
2862
|
+
if (options2.challengeRequired && !challengeToken) {
|
|
2863
|
+
status = "idle";
|
|
2864
|
+
submitError = CHALLENGE_INCOMPLETE_MESSAGE;
|
|
2865
|
+
notify();
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2713
2868
|
status = "submitting";
|
|
2714
2869
|
submitError = void 0;
|
|
2715
2870
|
notify();
|
|
@@ -2721,10 +2876,17 @@ function createFormController(options2) {
|
|
|
2721
2876
|
surface: options2.surface ?? "headless",
|
|
2722
2877
|
submissionKey,
|
|
2723
2878
|
draft: submitDraft ? { id: submitDraft.id, token: submitDraft.token } : void 0,
|
|
2724
|
-
respondent
|
|
2879
|
+
respondent,
|
|
2880
|
+
challengeToken
|
|
2725
2881
|
});
|
|
2726
2882
|
} catch (err) {
|
|
2727
2883
|
status = "idle";
|
|
2884
|
+
if (isFilloError(err) && err.code === "challenge_failed") {
|
|
2885
|
+
submitError = CHALLENGE_RETRY_MESSAGE;
|
|
2886
|
+
options2.onChallengeFailed?.();
|
|
2887
|
+
notify();
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2728
2890
|
submitError = submitFailureMessage(err);
|
|
2729
2891
|
notify();
|
|
2730
2892
|
throw err;
|
|
@@ -2833,7 +2995,7 @@ function needsExplicitSubmit(visible) {
|
|
|
2833
2995
|
}
|
|
2834
2996
|
function shouldAutoSubmit(field2, value, ctx) {
|
|
2835
2997
|
if (ctx.form.settings.submitMode !== "auto") return false;
|
|
2836
|
-
if (
|
|
2998
|
+
if (ctx.status !== "idle" || ctx.uploading) return false;
|
|
2837
2999
|
const hasDiscreteAnswer = (() => {
|
|
2838
3000
|
switch (field2.kind) {
|
|
2839
3001
|
case "select":
|
|
@@ -2851,7 +3013,9 @@ function shouldAutoSubmit(field2, value, ctx) {
|
|
|
2851
3013
|
if (!hasDiscreteAnswer) return false;
|
|
2852
3014
|
if (value === ctx.data[field2.id]) return false;
|
|
2853
3015
|
const nextData = { ...ctx.data, [field2.id]: value };
|
|
2854
|
-
const
|
|
3016
|
+
const page = ctx.form.pages.find((p) => p.blocks.some((b) => b.id === field2.id));
|
|
3017
|
+
if (!page || !isTerminalPage(ctx.form, page.id, nextData)) return false;
|
|
3018
|
+
const interactive = reachableFields(ctx.form, nextData).filter((f) => f.kind !== "hidden");
|
|
2855
3019
|
if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
|
|
2856
3020
|
return validateResponse(ctx.form, nextData).ok;
|
|
2857
3021
|
}
|
|
@@ -2877,7 +3041,8 @@ var FILLO_SLOTS = [
|
|
|
2877
3041
|
"footer",
|
|
2878
3042
|
"button",
|
|
2879
3043
|
"success",
|
|
2880
|
-
"resume"
|
|
3044
|
+
"resume",
|
|
3045
|
+
"turnstile"
|
|
2881
3046
|
];
|
|
2882
3047
|
var FILLO_DATA_ATTRS = {
|
|
2883
3048
|
/** Slot name, on every slot element: `data-fillo="control"`. */
|
|
@@ -2985,7 +3150,7 @@ function formSchemasEqual(left, right) {
|
|
|
2985
3150
|
var syncTtlMs = (status) => status === "published" ? 36e5 : 6e4;
|
|
2986
3151
|
var isVolatileSync = (result) => Boolean(result.staged || result.resolvedSchema || result.syncError);
|
|
2987
3152
|
function storageKey(client, handle) {
|
|
2988
|
-
return `fillo:sync:
|
|
3153
|
+
return `fillo:sync:v3:${client.baseUrl}|${client.key}|${handle}`;
|
|
2989
3154
|
}
|
|
2990
3155
|
function readStoredSync(client, handle, hash) {
|
|
2991
3156
|
try {
|
|
@@ -3003,6 +3168,7 @@ function readStoredSync(client, handle, hash) {
|
|
|
3003
3168
|
}
|
|
3004
3169
|
function writeStoredSync(client, handle, hash, r) {
|
|
3005
3170
|
if (r.staged || r.resolvedSchema || r.syncError) return;
|
|
3171
|
+
if (r.challenge) return;
|
|
3006
3172
|
try {
|
|
3007
3173
|
globalThis.localStorage?.setItem(
|
|
3008
3174
|
storageKey(client, handle),
|
|
@@ -3415,6 +3581,7 @@ export {
|
|
|
3415
3581
|
DEFAULT_FIELD_STRINGS,
|
|
3416
3582
|
DEFAULT_STRINGS,
|
|
3417
3583
|
DRAFT_KINDS,
|
|
3584
|
+
FILLO_CHALLENGE_MIN_SDK_VERSION,
|
|
3418
3585
|
FILLO_DATA_ATTRS,
|
|
3419
3586
|
FILLO_MIN_SDK_VERSION,
|
|
3420
3587
|
FILLO_SCHEMA_VERSION,
|
|
@@ -3433,6 +3600,7 @@ export {
|
|
|
3433
3600
|
allFields,
|
|
3434
3601
|
assembleForm,
|
|
3435
3602
|
codeFormFromJsx,
|
|
3603
|
+
conditionsMet,
|
|
3436
3604
|
contentHash,
|
|
3437
3605
|
countryByDialCode,
|
|
3438
3606
|
countryByIso,
|
|
@@ -3454,6 +3622,7 @@ export {
|
|
|
3454
3622
|
isField,
|
|
3455
3623
|
isFilloError,
|
|
3456
3624
|
isPossiblePhone,
|
|
3625
|
+
isTerminalPage,
|
|
3457
3626
|
needsExplicitSubmit,
|
|
3458
3627
|
normalizeFormSchema,
|
|
3459
3628
|
normalizeFormTheme,
|
|
@@ -3463,6 +3632,11 @@ export {
|
|
|
3463
3632
|
positionPhonePopover,
|
|
3464
3633
|
prefillFromParams,
|
|
3465
3634
|
provisionWorkspace,
|
|
3635
|
+
reachableFieldIds,
|
|
3636
|
+
reachableFields,
|
|
3637
|
+
reachablePageIds,
|
|
3638
|
+
reachablePageSequence,
|
|
3639
|
+
resolveNextPage,
|
|
3466
3640
|
resolveSlotClass,
|
|
3467
3641
|
resolveStrings,
|
|
3468
3642
|
resolveText,
|