@secmia/openui-flow 4.0.0 → 4.1.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.mjs CHANGED
@@ -248,10 +248,78 @@ function clearPersistedState(persistence) {
248
248
  }
249
249
  storage.removeItem(persistence.key);
250
250
  }
251
- async function assertValid(result) {
252
- const message = await result;
253
- if (typeof message === "string" && message.trim().length > 0) {
254
- throw new Error(message);
251
+ var FlowValidationError = class extends Error {
252
+ constructor(message, fieldErrors) {
253
+ super(message);
254
+ this.name = "FlowValidationError";
255
+ this.fieldErrors = fieldErrors;
256
+ }
257
+ };
258
+ function toValidationError(issue, fallbackField) {
259
+ const field = issue.field ?? fallbackField;
260
+ if (field) {
261
+ return new FlowValidationError(issue.message, { [field]: issue.message });
262
+ }
263
+ return new FlowValidationError(issue.message, {});
264
+ }
265
+ function normalizeSchemaIssues(error, fallbackField) {
266
+ const fieldErrors = {};
267
+ const issues = error?.issues ?? [];
268
+ for (const issue of issues) {
269
+ const path = issue.path?.length ? issue.path.map(String).join(".") : fallbackField;
270
+ const message = issue.message?.trim() || error?.message || "Validation failed.";
271
+ if (path && !fieldErrors[path]) {
272
+ fieldErrors[path] = message;
273
+ }
274
+ }
275
+ if (Object.keys(fieldErrors).length === 0 && fallbackField) {
276
+ fieldErrors[fallbackField] = error?.message || "Validation failed.";
277
+ }
278
+ return fieldErrors;
279
+ }
280
+ async function assertValid(result, fallbackField) {
281
+ const outcome = await result;
282
+ if (!outcome) {
283
+ return;
284
+ }
285
+ if (typeof outcome === "string") {
286
+ const message = outcome.trim();
287
+ if (message.length > 0) {
288
+ throw new FlowValidationError(message, fallbackField ? { [fallbackField]: message } : {});
289
+ }
290
+ return;
291
+ }
292
+ if (outcome.message.trim().length > 0) {
293
+ throw toValidationError(outcome, fallbackField);
294
+ }
295
+ }
296
+ async function assertSchema(schema, value, fallbackField) {
297
+ if (!schema) {
298
+ return;
299
+ }
300
+ if (schema.safeParse) {
301
+ const result = schema.safeParse(value);
302
+ if (!result.success) {
303
+ const fieldErrors = normalizeSchemaIssues(result.error, fallbackField);
304
+ throw new FlowValidationError(result.error?.message || "Validation failed.", fieldErrors);
305
+ }
306
+ return;
307
+ }
308
+ if (!schema.parse) {
309
+ return;
310
+ }
311
+ try {
312
+ schema.parse(value);
313
+ } catch (error) {
314
+ const normalized = error;
315
+ const fieldErrors = normalizeSchemaIssues(
316
+ {
317
+ message: normalized?.message,
318
+ issues: normalized?.issues
319
+ },
320
+ fallbackField
321
+ );
322
+ throw new FlowValidationError(normalized?.message || "Validation failed.", fieldErrors);
255
323
  }
256
324
  }
257
325
  function resolveStyles(unstyled, styleOverrides) {
@@ -268,26 +336,20 @@ function resolveStyles(unstyled, styleOverrides) {
268
336
  }
269
337
  return resolved;
270
338
  }
271
- function AdaptiveFlow({
339
+ function useAdaptiveFlow({
272
340
  adapter,
273
341
  requirements,
274
342
  requirementGraph,
275
343
  requirementGraphConfig,
276
344
  requirementResolvers,
277
345
  completeStep,
278
- stepTitles,
279
- renderStep,
280
- stepRegistry,
281
346
  initialValue,
282
347
  onComplete,
283
348
  onError,
284
349
  onStepTransition,
285
- className,
286
- classNames,
287
- styles,
288
- unstyled = false,
289
350
  persistence,
290
- validators
351
+ validators,
352
+ schemas
291
353
  }) {
292
354
  const normalizedRequirements = React.useMemo(
293
355
  () => requirements ?? defaultRequirements,
@@ -321,7 +383,6 @@ function AdaptiveFlow({
321
383
  requirementGraphConfig?.dependencies
322
384
  ]
323
385
  );
324
- const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
325
386
  const [context, setContext] = React.useState(() => withDefaults(initialValue));
326
387
  const [step, setStep] = React.useState(normalizedCompleteStep);
327
388
  const [missingRequirements, setMissingRequirements] = React.useState([]);
@@ -329,6 +390,7 @@ function AdaptiveFlow({
329
390
  const [busy, setBusy] = React.useState(false);
330
391
  const [message, setMessage] = React.useState(null);
331
392
  const [errorMessage, setErrorMessage] = React.useState(null);
393
+ const [fieldErrors, setFieldErrors] = React.useState({});
332
394
  const [oauthPendingProvider, setOauthPendingProvider] = React.useState(null);
333
395
  const [persistenceHydrated, setPersistenceHydrated] = React.useState(!persistence);
334
396
  const attemptByStepRef = React.useRef({});
@@ -356,14 +418,14 @@ function AdaptiveFlow({
356
418
  let isCancelled = false;
357
419
  const currentEvaluation = ++evaluationRef.current;
358
420
  void (async () => {
359
- const [missing2, next] = await Promise.all([
421
+ const [missing, next] = await Promise.all([
360
422
  getMissingRequirements(context, graph),
361
423
  evaluateNextStep(context, graph, normalizedCompleteStep)
362
424
  ]);
363
425
  if (isCancelled || currentEvaluation !== evaluationRef.current) {
364
426
  return;
365
427
  }
366
- setMissingRequirements(missing2);
428
+ setMissingRequirements(missing);
367
429
  setStep(next);
368
430
  const from = previousStepRef.current;
369
431
  const attemptKey = String(next);
@@ -383,6 +445,7 @@ function AdaptiveFlow({
383
445
  return;
384
446
  }
385
447
  const normalized = toError(error);
448
+ setFieldErrors({});
386
449
  setErrorMessage(normalized.message);
387
450
  onError?.(normalized);
388
451
  });
@@ -417,12 +480,21 @@ function AdaptiveFlow({
417
480
  async (job) => {
418
481
  setBusy(true);
419
482
  setErrorMessage(null);
483
+ setFieldErrors({});
420
484
  try {
421
485
  await job();
422
486
  } catch (error) {
423
- const normalized = toError(error);
424
- setErrorMessage(normalized.message);
425
- onError?.(normalized);
487
+ if (error instanceof FlowValidationError) {
488
+ setFieldErrors(error.fieldErrors);
489
+ if (Object.keys(error.fieldErrors).length === 0) {
490
+ setErrorMessage(error.message);
491
+ onError?.(error);
492
+ }
493
+ } else {
494
+ const normalized = toError(error);
495
+ setErrorMessage(normalized.message);
496
+ onError?.(normalized);
497
+ }
426
498
  } finally {
427
499
  setBusy(false);
428
500
  }
@@ -458,8 +530,9 @@ function AdaptiveFlow({
458
530
  return;
459
531
  }
460
532
  void run(async () => {
533
+ await assertSchema(schemas?.email, email, "email");
461
534
  if (validators?.email) {
462
- await assertValid(validators.email(email, { context }));
535
+ await assertValid(validators.email(email, { context }), "email");
463
536
  }
464
537
  const identity = await adapter?.lookupByEmail?.(email) ?? null;
465
538
  patchBaseContext({
@@ -491,8 +564,9 @@ function AdaptiveFlow({
491
564
  return;
492
565
  }
493
566
  void run(async () => {
567
+ await assertSchema(schemas?.otp, code, "otp");
494
568
  if (validators?.otp) {
495
- await assertValid(validators.otp(code, { context, email: context.email }));
569
+ await assertValid(validators.otp(code, { context, email: context.email }), "otp");
496
570
  }
497
571
  if (adapter?.verifyOtp) {
498
572
  await adapter.verifyOtp(context.email, code);
@@ -506,8 +580,12 @@ function AdaptiveFlow({
506
580
  return;
507
581
  }
508
582
  void run(async () => {
583
+ await assertSchema(schemas?.password, password, "password");
509
584
  if (validators?.password) {
510
- await assertValid(validators.password(password, { context, hasPassword: context.hasPassword }));
585
+ await assertValid(
586
+ validators.password(password, { context, hasPassword: context.hasPassword }),
587
+ "password"
588
+ );
511
589
  }
512
590
  if (context.hasPassword) {
513
591
  if (adapter?.signInWithPassword) {
@@ -524,8 +602,9 @@ function AdaptiveFlow({
524
602
  };
525
603
  const handleProfile = (profile) => {
526
604
  void run(async () => {
605
+ await assertSchema(schemas?.profile, profile, "profile");
527
606
  if (validators?.profile) {
528
- await assertValid(validators.profile(profile, { context }));
607
+ await assertValid(validators.profile(profile, { context }), "profile");
529
608
  }
530
609
  const next = mergeContext(context, {
531
610
  profile: {
@@ -543,8 +622,9 @@ function AdaptiveFlow({
543
622
  };
544
623
  const handleTos = () => {
545
624
  void run(async () => {
625
+ await assertSchema(schemas?.tos, true, "tos");
546
626
  if (validators?.tos) {
547
- await assertValid(validators.tos(true, { context }));
627
+ await assertValid(validators.tos(true, { context }), "tos");
548
628
  }
549
629
  const next = mergeContext(context, { agreedToTos: true });
550
630
  if (adapter?.acceptTos) {
@@ -565,7 +645,84 @@ function AdaptiveFlow({
565
645
  await startOAuth(provider, context);
566
646
  });
567
647
  };
568
- const missing = missingRequirements;
648
+ return {
649
+ context,
650
+ step,
651
+ completeStep: normalizedCompleteStep,
652
+ requirements: normalizedRequirements,
653
+ missingRequirements,
654
+ transitions,
655
+ busy,
656
+ message,
657
+ errorMessage,
658
+ fieldErrors,
659
+ setContextPatch: patchContext,
660
+ run,
661
+ handleEmail,
662
+ handleOtp,
663
+ handlePassword,
664
+ handleProfile,
665
+ handleTos,
666
+ handleOAuth
667
+ };
668
+ }
669
+ function AdaptiveFlow({
670
+ adapter,
671
+ requirements,
672
+ requirementGraph,
673
+ requirementGraphConfig,
674
+ requirementResolvers,
675
+ completeStep,
676
+ stepTitles,
677
+ renderStep,
678
+ stepRegistry,
679
+ initialValue,
680
+ onComplete,
681
+ onError,
682
+ onStepTransition,
683
+ className,
684
+ classNames,
685
+ styles,
686
+ unstyled = false,
687
+ persistence,
688
+ validators,
689
+ schemas
690
+ }) {
691
+ const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
692
+ const {
693
+ context,
694
+ step,
695
+ completeStep: normalizedCompleteStep,
696
+ requirements: normalizedRequirements,
697
+ missingRequirements: missing,
698
+ transitions,
699
+ busy,
700
+ message,
701
+ errorMessage,
702
+ fieldErrors,
703
+ setContextPatch: patchContext,
704
+ run,
705
+ handleEmail,
706
+ handleOtp,
707
+ handlePassword,
708
+ handleProfile,
709
+ handleTos,
710
+ handleOAuth
711
+ } = useAdaptiveFlow({
712
+ adapter,
713
+ requirements,
714
+ requirementGraph,
715
+ requirementGraphConfig,
716
+ requirementResolvers,
717
+ completeStep,
718
+ initialValue,
719
+ onComplete,
720
+ onError,
721
+ onStepTransition,
722
+ persistence,
723
+ validators,
724
+ schemas
725
+ });
569
726
  const needsJobTitle = normalizedRequirements.includes("has_job_title");
570
727
  const stepLabel = stepTitles?.[step] ?? defaultStepTitle[step] ?? step;
571
728
  const defaultView = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -574,6 +731,7 @@ function AdaptiveFlow({
574
731
  {
575
732
  onSubmit: handleEmail,
576
733
  disabled: busy,
734
+ error: fieldErrors.email,
577
735
  styles: uiStyles,
578
736
  classNames
579
737
  }
@@ -584,6 +742,7 @@ function AdaptiveFlow({
584
742
  onSubmit: handleOtp,
585
743
  email: context.email ?? "your email",
586
744
  disabled: busy,
745
+ error: fieldErrors.otp,
587
746
  styles: uiStyles,
588
747
  classNames
589
748
  }
@@ -594,6 +753,7 @@ function AdaptiveFlow({
594
753
  onSubmit: handlePassword,
595
754
  disabled: busy,
596
755
  hasPassword: context.hasPassword,
756
+ error: fieldErrors.password,
597
757
  styles: uiStyles,
598
758
  classNames
599
759
  }
@@ -605,6 +765,7 @@ function AdaptiveFlow({
605
765
  requireJobTitle: needsJobTitle,
606
766
  onSubmit: handleProfile,
607
767
  disabled: busy,
768
+ errors: fieldErrors,
608
769
  styles: uiStyles,
609
770
  classNames
610
771
  }
@@ -614,6 +775,7 @@ function AdaptiveFlow({
614
775
  {
615
776
  onSubmit: handleTos,
616
777
  disabled: busy,
778
+ error: fieldErrors.tos,
617
779
  styles: uiStyles,
618
780
  classNames
619
781
  }
@@ -631,6 +793,7 @@ function AdaptiveFlow({
631
793
  busy,
632
794
  message,
633
795
  errorMessage,
796
+ fieldErrors,
634
797
  missingRequirements: missing,
635
798
  requirements: normalizedRequirements,
636
799
  defaultView,
@@ -645,6 +808,7 @@ function AdaptiveFlow({
645
808
  busy,
646
809
  message,
647
810
  errorMessage,
811
+ fieldErrors,
648
812
  missingRequirements: missing,
649
813
  requirements: normalizedRequirements,
650
814
  setContextPatch: patchContext,
@@ -697,8 +861,15 @@ function AdaptiveFlow({
697
861
  ] })
698
862
  ] });
699
863
  }
700
- function EmailBlock({ onSubmit, disabled, styles, classNames }) {
864
+ function EmailBlock({
865
+ onSubmit,
866
+ disabled,
867
+ error,
868
+ styles,
869
+ classNames
870
+ }) {
701
871
  const [email, setEmail] = React.useState("");
872
+ const errorId = "adaptive-flow-email-error";
702
873
  return /* @__PURE__ */ jsxs(
703
874
  "form",
704
875
  {
@@ -719,10 +890,13 @@ function EmailBlock({ onSubmit, disabled, styles, classNames }) {
719
890
  placeholder: "Enter your email",
720
891
  value: email,
721
892
  onChange: (event) => setEmail(event.target.value),
893
+ "aria-invalid": Boolean(error),
894
+ "aria-describedby": error ? errorId : void 0,
722
895
  required: true
723
896
  }
724
897
  ),
725
- /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
898
+ /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" }),
899
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
726
900
  ]
727
901
  }
728
902
  );
@@ -731,10 +905,12 @@ function OtpBlock({
731
905
  onSubmit,
732
906
  disabled,
733
907
  email,
908
+ error,
734
909
  styles,
735
910
  classNames
736
911
  }) {
737
912
  const [code, setCode] = React.useState("");
913
+ const errorId = "adaptive-flow-otp-error";
738
914
  return /* @__PURE__ */ jsxs(
739
915
  "form",
740
916
  {
@@ -760,6 +936,8 @@ function OtpBlock({
760
936
  placeholder: "Enter 6-digit code",
761
937
  value: code,
762
938
  onChange: (event) => setCode(event.target.value.replace(/\D/g, "").slice(0, 6)),
939
+ "aria-invalid": Boolean(error),
940
+ "aria-describedby": error ? errorId : void 0,
763
941
  required: true,
764
942
  maxLength: 6,
765
943
  pattern: "[0-9]{6}"
@@ -774,7 +952,8 @@ function OtpBlock({
774
952
  type: "submit",
775
953
  children: "Verify"
776
954
  }
777
- )
955
+ ),
956
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
778
957
  ] })
779
958
  ]
780
959
  }
@@ -784,10 +963,12 @@ function PasswordBlock({
784
963
  onSubmit,
785
964
  disabled,
786
965
  hasPassword,
966
+ error,
787
967
  styles,
788
968
  classNames
789
969
  }) {
790
970
  const [password, setPassword] = React.useState("");
971
+ const errorId = "adaptive-flow-password-error";
791
972
  return /* @__PURE__ */ jsxs(
792
973
  "form",
793
974
  {
@@ -808,11 +989,14 @@ function PasswordBlock({
808
989
  placeholder: hasPassword ? "Enter your password" : "Create a password",
809
990
  value: password,
810
991
  onChange: (event) => setPassword(event.target.value),
992
+ "aria-invalid": Boolean(error),
993
+ "aria-describedby": error ? errorId : void 0,
811
994
  required: true,
812
995
  minLength: 8
813
996
  }
814
997
  ),
815
- /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
998
+ /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" }),
999
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
816
1000
  ]
817
1001
  }
818
1002
  );
@@ -822,12 +1006,17 @@ function ProfileBlock({
822
1006
  disabled,
823
1007
  defaultValue,
824
1008
  requireJobTitle,
1009
+ errors,
825
1010
  styles,
826
1011
  classNames
827
1012
  }) {
828
1013
  const [firstName, setFirstName] = React.useState(defaultValue.firstName ?? "");
829
1014
  const [lastName, setLastName] = React.useState(defaultValue.lastName ?? "");
830
1015
  const [jobTitle, setJobTitle] = React.useState(defaultValue.jobTitle ?? "");
1016
+ const firstNameError = errors?.firstName ?? errors?.["profile.firstName"];
1017
+ const lastNameError = errors?.lastName ?? errors?.["profile.lastName"];
1018
+ const jobTitleError = errors?.jobTitle ?? errors?.["profile.jobTitle"];
1019
+ const profileError = errors?.profile;
831
1020
  return /* @__PURE__ */ jsxs(
832
1021
  "form",
833
1022
  {
@@ -853,6 +1042,7 @@ function ProfileBlock({
853
1042
  placeholder: "First name",
854
1043
  value: firstName,
855
1044
  onChange: (event) => setFirstName(event.target.value),
1045
+ "aria-invalid": Boolean(firstNameError),
856
1046
  required: true
857
1047
  }
858
1048
  ),
@@ -866,23 +1056,31 @@ function ProfileBlock({
866
1056
  placeholder: "Last name",
867
1057
  value: lastName,
868
1058
  onChange: (event) => setLastName(event.target.value),
1059
+ "aria-invalid": Boolean(lastNameError),
869
1060
  required: true
870
1061
  }
871
1062
  )
872
1063
  ] }),
873
- requireJobTitle ? /* @__PURE__ */ jsx(
874
- "input",
875
- {
876
- className: classNames?.input,
877
- style: styles.input,
878
- type: "text",
879
- autoComplete: "organization-title",
880
- placeholder: "Job title",
881
- value: jobTitle,
882
- onChange: (event) => setJobTitle(event.target.value),
883
- required: true
884
- }
885
- ) : null,
1064
+ firstNameError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: firstNameError }) : null,
1065
+ lastNameError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: lastNameError }) : null,
1066
+ requireJobTitle ? /* @__PURE__ */ jsxs(Fragment, { children: [
1067
+ /* @__PURE__ */ jsx(
1068
+ "input",
1069
+ {
1070
+ className: classNames?.input,
1071
+ style: styles.input,
1072
+ type: "text",
1073
+ autoComplete: "organization-title",
1074
+ placeholder: "Job title",
1075
+ value: jobTitle,
1076
+ onChange: (event) => setJobTitle(event.target.value),
1077
+ "aria-invalid": Boolean(jobTitleError),
1078
+ required: true
1079
+ }
1080
+ ),
1081
+ jobTitleError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: jobTitleError }) : null
1082
+ ] }) : null,
1083
+ profileError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: profileError }) : null,
886
1084
  /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
887
1085
  ]
888
1086
  }
@@ -891,6 +1089,7 @@ function ProfileBlock({
891
1089
  function TosBlock({
892
1090
  onSubmit,
893
1091
  disabled,
1092
+ error,
894
1093
  styles,
895
1094
  classNames
896
1095
  }) {
@@ -908,9 +1107,18 @@ function TosBlock({
908
1107
  },
909
1108
  children: [
910
1109
  /* @__PURE__ */ jsxs("label", { className: classNames?.checkboxRow, style: styles.checkboxRow, children: [
911
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked, onChange: (event) => setChecked(event.target.checked) }),
1110
+ /* @__PURE__ */ jsx(
1111
+ "input",
1112
+ {
1113
+ type: "checkbox",
1114
+ checked,
1115
+ onChange: (event) => setChecked(event.target.checked),
1116
+ "aria-invalid": Boolean(error)
1117
+ }
1118
+ ),
912
1119
  /* @__PURE__ */ jsx("span", { children: "I agree to the Terms of Service and Privacy Policy." })
913
1120
  ] }),
1121
+ error ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: error }) : null,
914
1122
  /* @__PURE__ */ jsx(
915
1123
  "button",
916
1124
  {
@@ -930,7 +1138,7 @@ function CompleteBlock({
930
1138
  classNames
931
1139
  }) {
932
1140
  return /* @__PURE__ */ jsxs("div", { className: classNames?.complete, style: styles.complete, children: [
933
- /* @__PURE__ */ jsx("strong", { children: "You are fully authenticated." }),
1141
+ /* @__PURE__ */ jsx("strong", { children: "You are all set." }),
934
1142
  /* @__PURE__ */ jsx("p", { className: classNames?.caption, style: styles.caption, children: "All requirements are complete." })
935
1143
  ] });
936
1144
  }
@@ -1068,6 +1276,7 @@ export {
1068
1276
  defaultRequirements,
1069
1277
  evaluateNextStep,
1070
1278
  getMissingRequirements,
1071
- initialContext
1279
+ initialContext,
1280
+ useAdaptiveFlow
1072
1281
  };
1073
1282
  //# sourceMappingURL=index.mjs.map