@secmia/openui-flow 4.0.1 → 4.2.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
@@ -26,6 +26,8 @@ var defaultRequirements = [
26
26
  DefaultAppRequirements.EMAIL_VERIFIED,
27
27
  DefaultAppRequirements.HAS_PASSWORD,
28
28
  DefaultAppRequirements.HAS_FIRST_NAME,
29
+ DefaultAppRequirements.HAS_LAST_NAME,
30
+ DefaultAppRequirements.HAS_JOB_TITLE,
29
31
  DefaultAppRequirements.ACCEPTED_TOS
30
32
  ];
31
33
  var initialContext = {
@@ -71,20 +73,66 @@ var defaultRequirementResolvers = {
71
73
  };
72
74
  function createRequirementGraph(requirements, resolvers, options) {
73
75
  const graph = [];
76
+ const requirementSet = new Set(requirements);
77
+ const resolverBackedRequirements = new Set(
78
+ requirements.filter((requirement) => Boolean(resolvers[requirement]))
79
+ );
74
80
  for (const requirement of requirements) {
75
81
  const resolver = resolvers[requirement];
76
82
  if (!resolver) {
77
83
  continue;
78
84
  }
85
+ const dependsOn = options?.dependencies?.[requirement] ?? [];
86
+ for (const dependency of dependsOn) {
87
+ if (!requirementSet.has(dependency)) {
88
+ throw new Error(
89
+ `Invalid dependency "${String(dependency)}" referenced by requirement "${String(requirement)}".`
90
+ );
91
+ }
92
+ if (!resolverBackedRequirements.has(dependency)) {
93
+ throw new Error(
94
+ `Dependency "${String(dependency)}" referenced by requirement "${String(requirement)}" has no resolver.`
95
+ );
96
+ }
97
+ }
79
98
  graph.push({
80
99
  requirement,
81
100
  step: resolver.step,
82
101
  isMet: resolver.isMet,
83
102
  when: options?.conditions?.[requirement],
84
103
  priority: options?.priorities?.[requirement] ?? 0,
85
- dependsOn: options?.dependencies?.[requirement] ?? []
104
+ dependsOn
86
105
  });
87
106
  }
107
+ const byRequirement = new Map(
108
+ graph.map((node) => [node.requirement, node])
109
+ );
110
+ const visited = /* @__PURE__ */ new Set();
111
+ const visiting = /* @__PURE__ */ new Set();
112
+ const dfs = (requirement, stack) => {
113
+ if (visited.has(requirement)) {
114
+ return;
115
+ }
116
+ if (visiting.has(requirement)) {
117
+ const cycleStart = stack.indexOf(requirement);
118
+ const cyclePath = [...stack.slice(cycleStart), requirement].map((value) => String(value)).join(" -> ");
119
+ throw new Error(`Circular dependency detected in requirement graph: ${cyclePath}`);
120
+ }
121
+ visiting.add(requirement);
122
+ const node = byRequirement.get(requirement);
123
+ if (node?.dependsOn?.length) {
124
+ for (const dep of node.dependsOn) {
125
+ if (byRequirement.has(dep)) {
126
+ dfs(dep, [...stack, requirement]);
127
+ }
128
+ }
129
+ }
130
+ visiting.delete(requirement);
131
+ visited.add(requirement);
132
+ };
133
+ for (const node of graph) {
134
+ dfs(node.requirement, []);
135
+ }
88
136
  return graph;
89
137
  }
90
138
  function createDefaultRequirementGraph(options) {
@@ -100,13 +148,63 @@ function createDefaultRequirementGraph(options) {
100
148
  });
101
149
  }
102
150
  function sortGraph(graph) {
103
- return [...graph].sort((left, right) => {
104
- const byPriority = (right.priority ?? 0) - (left.priority ?? 0);
151
+ const entries = graph.map((node, index) => ({ node, index }));
152
+ const byRequirement = new Map(entries.map((entry) => [entry.node.requirement, entry]));
153
+ const indegree = /* @__PURE__ */ new Map();
154
+ const adjacency = /* @__PURE__ */ new Map();
155
+ for (const entry of entries) {
156
+ indegree.set(entry.node.requirement, 0);
157
+ adjacency.set(entry.node.requirement, []);
158
+ }
159
+ for (const entry of entries) {
160
+ const current = entry.node.requirement;
161
+ for (const dependency of entry.node.dependsOn ?? []) {
162
+ const depEntry = byRequirement.get(dependency);
163
+ if (!depEntry) {
164
+ continue;
165
+ }
166
+ const outgoing = adjacency.get(depEntry.node.requirement);
167
+ if (outgoing) {
168
+ outgoing.push(current);
169
+ }
170
+ indegree.set(current, (indegree.get(current) ?? 0) + 1);
171
+ }
172
+ }
173
+ const compare = (left, right) => {
174
+ const byPriority = (right.node.priority ?? 0) - (left.node.priority ?? 0);
105
175
  if (byPriority !== 0) {
106
176
  return byPriority;
107
177
  }
108
- return 0;
109
- });
178
+ const byRequirementName = String(left.node.requirement).localeCompare(String(right.node.requirement));
179
+ if (byRequirementName !== 0) {
180
+ return byRequirementName;
181
+ }
182
+ return left.index - right.index;
183
+ };
184
+ const ready = entries.filter((entry) => (indegree.get(entry.node.requirement) ?? 0) === 0).sort(compare);
185
+ const ordered = [];
186
+ while (ready.length > 0) {
187
+ const current = ready.shift();
188
+ if (!current) {
189
+ break;
190
+ }
191
+ ordered.push(current.node);
192
+ for (const dependent of adjacency.get(current.node.requirement) ?? []) {
193
+ const next = (indegree.get(dependent) ?? 0) - 1;
194
+ indegree.set(dependent, next);
195
+ if (next === 0) {
196
+ const dependentEntry = byRequirement.get(dependent);
197
+ if (dependentEntry) {
198
+ ready.push(dependentEntry);
199
+ ready.sort(compare);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (ordered.length !== graph.length) {
205
+ throw new Error("Unable to topologically sort requirement graph. Check for dependency cycles.");
206
+ }
207
+ return ordered;
110
208
  }
111
209
  async function evaluateNextStep(context, graph, completeStep) {
112
210
  for (const node of sortGraph(graph)) {
@@ -147,6 +245,10 @@ async function getMissingRequirements(context, graph) {
147
245
 
148
246
  // src/AdaptiveFlow.tsx
149
247
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
248
+ var defaultOAuthProviders = [
249
+ { id: "google", label: "Continue with Google" },
250
+ { id: "apple", label: "Continue with Apple" }
251
+ ];
150
252
  var defaultStepTitle = {
151
253
  COLLECT_EMAIL: "Enter your email",
152
254
  VERIFY_OTP: "Verify your email",
@@ -155,6 +257,14 @@ var defaultStepTitle = {
155
257
  COLLECT_TOS: "Accept terms",
156
258
  COMPLETE: "Done"
157
259
  };
260
+ var builtInDefaultSteps = /* @__PURE__ */ new Set([
261
+ "COLLECT_EMAIL",
262
+ "VERIFY_OTP",
263
+ "COLLECT_PASSWORD",
264
+ "COLLECT_PROFILE",
265
+ "COLLECT_TOS",
266
+ "COMPLETE"
267
+ ]);
158
268
  var styleSlots = [
159
269
  "shell",
160
270
  "headerRow",
@@ -175,14 +285,24 @@ var styleSlots = [
175
285
  "oauthButton"
176
286
  ];
177
287
  function mergeContext(current, patch) {
178
- return {
179
- ...current,
180
- ...patch,
181
- profile: {
182
- ...current.profile,
183
- ...patch.profile
288
+ const mergeValue = (baseValue, patchValue) => {
289
+ if (Array.isArray(baseValue) || Array.isArray(patchValue)) {
290
+ return patchValue;
291
+ }
292
+ const baseIsObject = Boolean(baseValue) && typeof baseValue === "object";
293
+ const patchIsObject = Boolean(patchValue) && typeof patchValue === "object";
294
+ if (!baseIsObject || !patchIsObject) {
295
+ return patchValue === void 0 ? baseValue : patchValue;
184
296
  }
297
+ const baseObject = baseValue;
298
+ const patchObject = patchValue;
299
+ const result = { ...baseObject };
300
+ for (const key of Object.keys(patchObject)) {
301
+ result[key] = mergeValue(baseObject[key], patchObject[key]);
302
+ }
303
+ return result;
185
304
  };
305
+ return mergeValue(current, patch);
186
306
  }
187
307
  function withDefaults(initialValue) {
188
308
  if (!initialValue) {
@@ -194,7 +314,65 @@ function toError(error) {
194
314
  if (error instanceof Error) {
195
315
  return error;
196
316
  }
197
- return new Error("Unknown error while processing adaptive flow");
317
+ if (typeof error === "string") {
318
+ return new Error(error);
319
+ }
320
+ if (error && typeof error === "object") {
321
+ const maybeError = error;
322
+ const message = typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : "Unknown error while processing adaptive flow";
323
+ const normalized = new Error(message);
324
+ if (typeof maybeError.code === "string" && maybeError.code.trim().length > 0) {
325
+ normalized.name = maybeError.code;
326
+ }
327
+ normalized.cause = error;
328
+ return normalized;
329
+ }
330
+ return new Error(`Unknown error while processing adaptive flow: ${String(error)}`);
331
+ }
332
+ function sleep(ms) {
333
+ return new Promise((resolve) => {
334
+ setTimeout(resolve, ms);
335
+ });
336
+ }
337
+ function normalizeDelay(delay, fallback) {
338
+ if (typeof delay !== "number" || Number.isNaN(delay) || delay < 0) {
339
+ return fallback;
340
+ }
341
+ return delay;
342
+ }
343
+ function computeRetryDelay(policy, attempt) {
344
+ if (policy?.delay) {
345
+ return normalizeDelay(policy.delay(attempt), 0);
346
+ }
347
+ const initialDelayMs = normalizeDelay(policy?.initialDelayMs, 250);
348
+ const factor = typeof policy?.factor === "number" && policy.factor > 0 ? policy.factor : 2;
349
+ const maxDelayMs = normalizeDelay(policy?.maxDelayMs, Number.POSITIVE_INFINITY);
350
+ let delay = initialDelayMs * Math.pow(factor, Math.max(0, attempt - 1));
351
+ if (policy?.jitter) {
352
+ delay = delay * (0.5 + Math.random() * 0.5);
353
+ }
354
+ return Math.min(delay, maxDelayMs);
355
+ }
356
+ async function withRetry(operation, retryPolicy) {
357
+ if (!retryPolicy) {
358
+ return operation();
359
+ }
360
+ const maxAttempts = Math.max(1, Math.trunc(retryPolicy.maxAttempts ?? 3));
361
+ let lastError;
362
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
363
+ try {
364
+ return await operation();
365
+ } catch (error) {
366
+ lastError = error;
367
+ const normalized = toError(error);
368
+ const shouldRetry = retryPolicy.shouldRetry?.(normalized, attempt) ?? attempt < maxAttempts;
369
+ if (!shouldRetry || attempt === maxAttempts) {
370
+ throw normalized;
371
+ }
372
+ await sleep(computeRetryDelay(retryPolicy, attempt));
373
+ }
374
+ }
375
+ throw toError(lastError);
198
376
  }
199
377
  function cx(...names) {
200
378
  const value = names.filter(Boolean).join(" ").trim();
@@ -248,10 +426,78 @@ function clearPersistedState(persistence) {
248
426
  }
249
427
  storage.removeItem(persistence.key);
250
428
  }
251
- async function assertValid(result) {
252
- const message = await result;
253
- if (typeof message === "string" && message.trim().length > 0) {
254
- throw new Error(message);
429
+ var FlowValidationError = class extends Error {
430
+ constructor(message, fieldErrors) {
431
+ super(message);
432
+ this.name = "FlowValidationError";
433
+ this.fieldErrors = fieldErrors;
434
+ }
435
+ };
436
+ function toValidationError(issue, fallbackField) {
437
+ const field = issue.field ?? fallbackField;
438
+ if (field) {
439
+ return new FlowValidationError(issue.message, { [field]: issue.message });
440
+ }
441
+ return new FlowValidationError(issue.message, {});
442
+ }
443
+ function normalizeSchemaIssues(error, fallbackField) {
444
+ const fieldErrors = {};
445
+ const issues = error?.issues ?? [];
446
+ for (const issue of issues) {
447
+ const path = issue.path?.length ? issue.path.map(String).join(".") : fallbackField;
448
+ const message = issue.message?.trim() || error?.message || "Validation failed.";
449
+ if (path && !fieldErrors[path]) {
450
+ fieldErrors[path] = message;
451
+ }
452
+ }
453
+ if (Object.keys(fieldErrors).length === 0 && fallbackField) {
454
+ fieldErrors[fallbackField] = error?.message || "Validation failed.";
455
+ }
456
+ return fieldErrors;
457
+ }
458
+ async function assertValid(result, fallbackField) {
459
+ const outcome = await result;
460
+ if (!outcome) {
461
+ return;
462
+ }
463
+ if (typeof outcome === "string") {
464
+ const message = outcome.trim();
465
+ if (message.length > 0) {
466
+ throw new FlowValidationError(message, fallbackField ? { [fallbackField]: message } : {});
467
+ }
468
+ return;
469
+ }
470
+ if (outcome.message.trim().length > 0) {
471
+ throw toValidationError(outcome, fallbackField);
472
+ }
473
+ }
474
+ async function assertSchema(schema, value, fallbackField) {
475
+ if (!schema) {
476
+ return;
477
+ }
478
+ if (schema.safeParse) {
479
+ const result = schema.safeParse(value);
480
+ if (!result.success) {
481
+ const fieldErrors = normalizeSchemaIssues(result.error, fallbackField);
482
+ throw new FlowValidationError(result.error?.message || "Validation failed.", fieldErrors);
483
+ }
484
+ return;
485
+ }
486
+ if (!schema.parse) {
487
+ return;
488
+ }
489
+ try {
490
+ schema.parse(value);
491
+ } catch (error) {
492
+ const normalized = error;
493
+ const fieldErrors = normalizeSchemaIssues(
494
+ {
495
+ message: normalized?.message,
496
+ issues: normalized?.issues
497
+ },
498
+ fallbackField
499
+ );
500
+ throw new FlowValidationError(normalized?.message || "Validation failed.", fieldErrors);
255
501
  }
256
502
  }
257
503
  function resolveStyles(unstyled, styleOverrides) {
@@ -268,26 +514,22 @@ function resolveStyles(unstyled, styleOverrides) {
268
514
  }
269
515
  return resolved;
270
516
  }
271
- function AdaptiveFlow({
517
+ function useAdaptiveFlow({
272
518
  adapter,
273
519
  requirements,
274
520
  requirementGraph,
275
521
  requirementGraphConfig,
276
522
  requirementResolvers,
277
523
  completeStep,
278
- stepTitles,
279
- renderStep,
280
- stepRegistry,
281
524
  initialValue,
282
525
  onComplete,
283
526
  onError,
284
527
  onStepTransition,
285
- className,
286
- classNames,
287
- styles,
288
- unstyled = false,
289
528
  persistence,
290
- validators
529
+ validators,
530
+ schemas,
531
+ oauthProviders,
532
+ retryPolicy
291
533
  }) {
292
534
  const normalizedRequirements = React.useMemo(
293
535
  () => requirements ?? defaultRequirements,
@@ -321,20 +563,68 @@ function AdaptiveFlow({
321
563
  requirementGraphConfig?.dependencies
322
564
  ]
323
565
  );
324
- const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
566
+ const runtimeReducer = (state, action) => {
567
+ switch (action.type) {
568
+ case "evaluated":
569
+ return {
570
+ ...state,
571
+ step: action.step,
572
+ missingRequirements: action.missingRequirements,
573
+ transitions: [...state.transitions, action.transition].slice(-100)
574
+ };
575
+ case "set_busy":
576
+ return { ...state, busy: action.busy };
577
+ case "set_message":
578
+ return { ...state, message: action.message };
579
+ case "set_error":
580
+ return { ...state, errorMessage: action.errorMessage };
581
+ case "set_field_errors":
582
+ return { ...state, fieldErrors: action.fieldErrors };
583
+ case "start_job":
584
+ return { ...state, busy: true, errorMessage: null, fieldErrors: {} };
585
+ case "set_oauth_pending":
586
+ return { ...state, oauthPendingProvider: action.provider };
587
+ case "set_hydrated":
588
+ return { ...state, persistenceHydrated: action.hydrated };
589
+ default:
590
+ return state;
591
+ }
592
+ };
325
593
  const [context, setContext] = React.useState(() => withDefaults(initialValue));
326
- const [step, setStep] = React.useState(normalizedCompleteStep);
327
- const [missingRequirements, setMissingRequirements] = React.useState([]);
328
- const [transitions, setTransitions] = React.useState([]);
329
- const [busy, setBusy] = React.useState(false);
330
- const [message, setMessage] = React.useState(null);
331
- const [errorMessage, setErrorMessage] = React.useState(null);
332
- const [oauthPendingProvider, setOauthPendingProvider] = React.useState(null);
333
- const [persistenceHydrated, setPersistenceHydrated] = React.useState(!persistence);
594
+ const [runtime, dispatch] = React.useReducer(runtimeReducer, {
595
+ step: normalizedCompleteStep,
596
+ missingRequirements: [],
597
+ transitions: [],
598
+ busy: false,
599
+ message: null,
600
+ errorMessage: null,
601
+ fieldErrors: {},
602
+ oauthPendingProvider: null,
603
+ persistenceHydrated: !persistence
604
+ });
605
+ const {
606
+ step,
607
+ missingRequirements,
608
+ transitions,
609
+ busy,
610
+ message,
611
+ errorMessage,
612
+ fieldErrors,
613
+ oauthPendingProvider,
614
+ persistenceHydrated
615
+ } = runtime;
334
616
  const attemptByStepRef = React.useRef({});
335
617
  const previousStepRef = React.useRef(null);
336
618
  const evaluationRef = React.useRef(0);
337
619
  const completed = React.useRef(false);
620
+ const reportPersistenceError = React.useCallback(
621
+ (error, phase) => {
622
+ const normalized = toError(error);
623
+ persistence?.onError?.(normalized, phase);
624
+ onError?.(normalized);
625
+ },
626
+ [onError, persistence]
627
+ );
338
628
  React.useEffect(() => {
339
629
  if (!persistence) {
340
630
  return;
@@ -345,26 +635,24 @@ function AdaptiveFlow({
345
635
  setContext(mergeContext(withDefaults(initialValue), persisted.context));
346
636
  }
347
637
  if (persisted?.oauthPendingProvider) {
348
- setOauthPendingProvider(persisted.oauthPendingProvider);
638
+ dispatch({ type: "set_oauth_pending", provider: persisted.oauthPendingProvider });
349
639
  }
350
- } catch {
640
+ } catch (error) {
641
+ reportPersistenceError(error, "read");
351
642
  } finally {
352
- setPersistenceHydrated(true);
643
+ dispatch({ type: "set_hydrated", hydrated: true });
353
644
  }
354
- }, [initialValue, persistence]);
645
+ }, [initialValue, persistence, reportPersistenceError]);
355
646
  React.useEffect(() => {
356
- let isCancelled = false;
357
647
  const currentEvaluation = ++evaluationRef.current;
358
648
  void (async () => {
359
- const [missing2, next] = await Promise.all([
649
+ const [missing, next] = await Promise.all([
360
650
  getMissingRequirements(context, graph),
361
651
  evaluateNextStep(context, graph, normalizedCompleteStep)
362
652
  ]);
363
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
653
+ if (currentEvaluation !== evaluationRef.current) {
364
654
  return;
365
655
  }
366
- setMissingRequirements(missing2);
367
- setStep(next);
368
656
  const from = previousStepRef.current;
369
657
  const attemptKey = String(next);
370
658
  const nextAttempt = from === next ? (attemptByStepRef.current[attemptKey] ?? 0) + 1 : 1;
@@ -375,20 +663,23 @@ function AdaptiveFlow({
375
663
  at: Date.now(),
376
664
  attempt: nextAttempt
377
665
  };
378
- setTransitions((previous) => [...previous, transition].slice(-100));
666
+ dispatch({
667
+ type: "evaluated",
668
+ missingRequirements: missing,
669
+ step: next,
670
+ transition
671
+ });
379
672
  previousStepRef.current = next;
380
673
  onStepTransition?.(transition, context);
381
674
  })().catch((error) => {
382
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
675
+ if (currentEvaluation !== evaluationRef.current) {
383
676
  return;
384
677
  }
385
678
  const normalized = toError(error);
386
- setErrorMessage(normalized.message);
679
+ dispatch({ type: "set_field_errors", fieldErrors: {} });
680
+ dispatch({ type: "set_error", errorMessage: normalized.message });
387
681
  onError?.(normalized);
388
682
  });
389
- return () => {
390
- isCancelled = true;
391
- };
392
683
  }, [context, graph, normalizedCompleteStep, onError, onStepTransition]);
393
684
  React.useEffect(() => {
394
685
  if (step === normalizedCompleteStep) {
@@ -397,34 +688,46 @@ function AdaptiveFlow({
397
688
  onComplete?.(context);
398
689
  const shouldClearPersistence = persistence?.clearOnComplete ?? true;
399
690
  if (shouldClearPersistence) {
400
- clearPersistedState(persistence);
691
+ try {
692
+ clearPersistedState(persistence);
693
+ } catch (error) {
694
+ reportPersistenceError(error, "clear");
695
+ }
401
696
  }
402
697
  }
403
698
  } else {
404
699
  completed.current = false;
405
700
  }
406
- }, [context, normalizedCompleteStep, onComplete, persistence, step]);
701
+ }, [context, normalizedCompleteStep, onComplete, persistence, reportPersistenceError, step]);
407
702
  React.useEffect(() => {
408
703
  if (!persistence || !persistenceHydrated) {
409
704
  return;
410
705
  }
411
706
  try {
412
707
  writePersistedState(persistence, { context, oauthPendingProvider });
413
- } catch {
708
+ } catch (error) {
709
+ reportPersistenceError(error, "write");
414
710
  }
415
- }, [context, oauthPendingProvider, persistence, persistenceHydrated]);
711
+ }, [context, oauthPendingProvider, persistence, persistenceHydrated, reportPersistenceError]);
416
712
  const run = React.useCallback(
417
713
  async (job) => {
418
- setBusy(true);
419
- setErrorMessage(null);
714
+ dispatch({ type: "start_job" });
420
715
  try {
421
716
  await job();
422
717
  } catch (error) {
423
- const normalized = toError(error);
424
- setErrorMessage(normalized.message);
425
- onError?.(normalized);
718
+ if (error instanceof FlowValidationError) {
719
+ dispatch({ type: "set_field_errors", fieldErrors: error.fieldErrors });
720
+ if (Object.keys(error.fieldErrors).length === 0) {
721
+ dispatch({ type: "set_error", errorMessage: error.message });
722
+ onError?.(error);
723
+ }
724
+ } else {
725
+ const normalized = toError(error);
726
+ dispatch({ type: "set_error", errorMessage: normalized.message });
727
+ onError?.(normalized);
728
+ }
426
729
  } finally {
427
- setBusy(false);
730
+ dispatch({ type: "set_busy", busy: false });
428
731
  }
429
732
  },
430
733
  [onError]
@@ -432,37 +735,35 @@ function AdaptiveFlow({
432
735
  const patchContext = React.useCallback((patch) => {
433
736
  setContext((prev) => mergeContext(prev, patch));
434
737
  }, []);
435
- const patchBaseContext = React.useCallback(
436
- (patch) => {
437
- patchContext(patch);
438
- },
439
- [patchContext]
440
- );
441
738
  React.useEffect(() => {
442
739
  const completeOAuth = adapter?.completeOAuth;
443
740
  if (!oauthPendingProvider || !completeOAuth) {
444
741
  return;
445
742
  }
446
743
  void run(async () => {
447
- const patch = await completeOAuth(oauthPendingProvider, context);
744
+ const patch = await withRetry(
745
+ () => completeOAuth(oauthPendingProvider, context),
746
+ retryPolicy
747
+ );
448
748
  if (patch) {
449
749
  patchContext(patch);
450
750
  }
451
- setOauthPendingProvider(null);
452
- setMessage("OAuth sign-in completed.");
751
+ dispatch({ type: "set_oauth_pending", provider: null });
752
+ dispatch({ type: "set_message", message: "OAuth sign-in completed." });
453
753
  });
454
- }, [adapter, context, oauthPendingProvider, patchContext, run]);
754
+ }, [adapter, context, oauthPendingProvider, patchContext, retryPolicy, run]);
455
755
  const handleEmail = (emailInput) => {
456
756
  const email = emailInput.trim().toLowerCase();
457
757
  if (!email) {
458
758
  return;
459
759
  }
460
760
  void run(async () => {
761
+ await assertSchema(schemas?.email, email, "email");
461
762
  if (validators?.email) {
462
- await assertValid(validators.email(email, { context }));
763
+ await assertValid(validators.email(email, { context }), "email");
463
764
  }
464
- const identity = await adapter?.lookupByEmail?.(email) ?? null;
465
- patchBaseContext({
765
+ const identity = adapter?.lookupByEmail ? await withRetry(() => adapter.lookupByEmail(email), retryPolicy) : null;
766
+ patchContext({
466
767
  email,
467
768
  hasPassword: Boolean(identity?.hasPassword),
468
769
  isVerified: Boolean(identity?.isVerified),
@@ -474,15 +775,23 @@ function AdaptiveFlow({
474
775
  }
475
776
  });
476
777
  if (identity?.accountExists && identity.hasPassword) {
477
- setMessage("Welcome back. Enter your password to continue.");
778
+ dispatch({ type: "set_message", message: "Welcome back. Enter your password to continue." });
478
779
  return;
479
780
  }
480
781
  if (adapter?.requestOtp) {
481
- await adapter.requestOtp(email);
482
- setMessage("We sent a 6-digit code to your inbox.");
782
+ await withRetry(() => adapter.requestOtp(email), retryPolicy);
783
+ dispatch({ type: "set_message", message: "We sent a 6-digit code to your inbox." });
483
784
  } else {
484
- patchBaseContext({ isVerified: true });
485
- setMessage("No OTP adapter configured. Email verification was skipped.");
785
+ const env = globalThis.process?.env?.NODE_ENV;
786
+ const isDev = env !== "production";
787
+ if (!isDev) {
788
+ throw new Error("OTP adapter is required in production. Provide adapter.requestOtp.");
789
+ }
790
+ patchContext({ isVerified: true });
791
+ dispatch({
792
+ type: "set_message",
793
+ message: "No OTP adapter configured. Verification was skipped in development mode."
794
+ });
486
795
  }
487
796
  });
488
797
  };
@@ -491,14 +800,15 @@ function AdaptiveFlow({
491
800
  return;
492
801
  }
493
802
  void run(async () => {
803
+ await assertSchema(schemas?.otp, code, "otp");
494
804
  if (validators?.otp) {
495
- await assertValid(validators.otp(code, { context, email: context.email }));
805
+ await assertValid(validators.otp(code, { context, email: context.email }), "otp");
496
806
  }
497
807
  if (adapter?.verifyOtp) {
498
- await adapter.verifyOtp(context.email, code);
808
+ await withRetry(() => adapter.verifyOtp(context.email, code), retryPolicy);
499
809
  }
500
- patchBaseContext({ isVerified: true });
501
- setMessage("Email verified.");
810
+ patchContext({ isVerified: true });
811
+ dispatch({ type: "set_message", message: "Email verified." });
502
812
  });
503
813
  };
504
814
  const handlePassword = (password) => {
@@ -506,26 +816,34 @@ function AdaptiveFlow({
506
816
  return;
507
817
  }
508
818
  void run(async () => {
819
+ await assertSchema(schemas?.password, password, "password");
509
820
  if (validators?.password) {
510
- await assertValid(validators.password(password, { context, hasPassword: context.hasPassword }));
821
+ await assertValid(
822
+ validators.password(password, { context, hasPassword: context.hasPassword }),
823
+ "password"
824
+ );
511
825
  }
512
826
  if (context.hasPassword) {
513
827
  if (adapter?.signInWithPassword) {
514
- await adapter.signInWithPassword(context.email, password);
828
+ await withRetry(
829
+ () => adapter.signInWithPassword(context.email, password),
830
+ retryPolicy
831
+ );
515
832
  }
516
833
  } else {
517
834
  if (adapter?.createPassword) {
518
- await adapter.createPassword(password);
835
+ await withRetry(() => adapter.createPassword(password), retryPolicy);
519
836
  }
520
- patchBaseContext({ hasPassword: true });
837
+ patchContext({ hasPassword: true });
521
838
  }
522
- setMessage("Password step complete.");
839
+ dispatch({ type: "set_message", message: "Password step complete." });
523
840
  });
524
841
  };
525
842
  const handleProfile = (profile) => {
526
843
  void run(async () => {
844
+ await assertSchema(schemas?.profile, profile, "profile");
527
845
  if (validators?.profile) {
528
- await assertValid(validators.profile(profile, { context }));
846
+ await assertValid(validators.profile(profile, { context }), "profile");
529
847
  }
530
848
  const next = mergeContext(context, {
531
849
  profile: {
@@ -535,23 +853,24 @@ function AdaptiveFlow({
535
853
  }
536
854
  });
537
855
  if (adapter?.saveProfile) {
538
- await adapter.saveProfile(next);
856
+ await withRetry(() => adapter.saveProfile(next), retryPolicy);
539
857
  }
540
858
  patchContext({ profile: next.profile });
541
- setMessage("Profile saved.");
859
+ dispatch({ type: "set_message", message: "Profile saved." });
542
860
  });
543
861
  };
544
862
  const handleTos = () => {
545
863
  void run(async () => {
864
+ await assertSchema(schemas?.tos, true, "tos");
546
865
  if (validators?.tos) {
547
- await assertValid(validators.tos(true, { context }));
866
+ await assertValid(validators.tos(true, { context }), "tos");
548
867
  }
549
868
  const next = mergeContext(context, { agreedToTos: true });
550
869
  if (adapter?.acceptTos) {
551
- await adapter.acceptTos(next);
870
+ await withRetry(() => adapter.acceptTos(next), retryPolicy);
552
871
  }
553
- patchBaseContext({ agreedToTos: true });
554
- setMessage("Terms accepted.");
872
+ patchContext({ agreedToTos: true });
873
+ dispatch({ type: "set_message", message: "Terms accepted." });
555
874
  });
556
875
  };
557
876
  const handleOAuth = (provider) => {
@@ -560,12 +879,97 @@ function AdaptiveFlow({
560
879
  return;
561
880
  }
562
881
  void run(async () => {
563
- setOauthPendingProvider(provider);
564
- setMessage(`Starting ${provider} sign-in...`);
565
- await startOAuth(provider, context);
882
+ dispatch({ type: "set_oauth_pending", provider });
883
+ dispatch({ type: "set_message", message: `Starting ${provider} sign-in...` });
884
+ await withRetry(() => startOAuth(provider, context), retryPolicy);
566
885
  });
567
886
  };
568
- const missing = missingRequirements;
887
+ return {
888
+ context,
889
+ step,
890
+ completeStep: normalizedCompleteStep,
891
+ requirements: normalizedRequirements,
892
+ missingRequirements,
893
+ transitions,
894
+ busy,
895
+ message,
896
+ errorMessage,
897
+ fieldErrors,
898
+ setContextPatch: patchContext,
899
+ run,
900
+ handleEmail,
901
+ handleOtp,
902
+ handlePassword,
903
+ handleProfile,
904
+ handleTos,
905
+ handleOAuth
906
+ };
907
+ }
908
+ function AdaptiveFlow({
909
+ adapter,
910
+ requirements,
911
+ requirementGraph,
912
+ requirementGraphConfig,
913
+ requirementResolvers,
914
+ completeStep,
915
+ stepTitles,
916
+ renderStep,
917
+ stepRegistry,
918
+ initialValue,
919
+ onComplete,
920
+ onError,
921
+ onStepTransition,
922
+ className,
923
+ classNames,
924
+ styles,
925
+ unstyled = false,
926
+ persistence,
927
+ validators,
928
+ schemas,
929
+ oauthProviders,
930
+ retryPolicy
931
+ }) {
932
+ const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
933
+ const {
934
+ context,
935
+ step,
936
+ completeStep: normalizedCompleteStep,
937
+ requirements: normalizedRequirements,
938
+ missingRequirements: missing,
939
+ transitions,
940
+ busy,
941
+ message,
942
+ errorMessage,
943
+ fieldErrors,
944
+ setContextPatch: patchContext,
945
+ run,
946
+ handleEmail,
947
+ handleOtp,
948
+ handlePassword,
949
+ handleProfile,
950
+ handleTos,
951
+ handleOAuth
952
+ } = useAdaptiveFlow({
953
+ adapter,
954
+ requirements,
955
+ requirementGraph,
956
+ requirementGraphConfig,
957
+ requirementResolvers,
958
+ completeStep,
959
+ initialValue,
960
+ onComplete,
961
+ onError,
962
+ onStepTransition,
963
+ persistence,
964
+ validators,
965
+ schemas,
966
+ oauthProviders,
967
+ retryPolicy
968
+ });
969
+ const normalizedOAuthProviders = React.useMemo(
970
+ () => oauthProviders && oauthProviders.length > 0 ? oauthProviders : defaultOAuthProviders,
971
+ [oauthProviders]
972
+ );
569
973
  const needsJobTitle = normalizedRequirements.includes("has_job_title");
570
974
  const stepLabel = stepTitles?.[step] ?? defaultStepTitle[step] ?? step;
571
975
  const defaultView = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -574,6 +978,7 @@ function AdaptiveFlow({
574
978
  {
575
979
  onSubmit: handleEmail,
576
980
  disabled: busy,
981
+ error: fieldErrors.email,
577
982
  styles: uiStyles,
578
983
  classNames
579
984
  }
@@ -584,6 +989,7 @@ function AdaptiveFlow({
584
989
  onSubmit: handleOtp,
585
990
  email: context.email ?? "your email",
586
991
  disabled: busy,
992
+ error: fieldErrors.otp,
587
993
  styles: uiStyles,
588
994
  classNames
589
995
  }
@@ -594,6 +1000,7 @@ function AdaptiveFlow({
594
1000
  onSubmit: handlePassword,
595
1001
  disabled: busy,
596
1002
  hasPassword: context.hasPassword,
1003
+ error: fieldErrors.password,
597
1004
  styles: uiStyles,
598
1005
  classNames
599
1006
  }
@@ -605,6 +1012,7 @@ function AdaptiveFlow({
605
1012
  requireJobTitle: needsJobTitle,
606
1013
  onSubmit: handleProfile,
607
1014
  disabled: busy,
1015
+ errors: fieldErrors,
608
1016
  styles: uiStyles,
609
1017
  classNames
610
1018
  }
@@ -614,12 +1022,13 @@ function AdaptiveFlow({
614
1022
  {
615
1023
  onSubmit: handleTos,
616
1024
  disabled: busy,
1025
+ error: fieldErrors.tos,
617
1026
  styles: uiStyles,
618
1027
  classNames
619
1028
  }
620
1029
  ) : null,
621
1030
  step === normalizedCompleteStep ? /* @__PURE__ */ jsx(CompleteBlock, { styles: uiStyles, classNames }) : null,
622
- step !== "COLLECT_EMAIL" && step !== "VERIFY_OTP" && step !== "COLLECT_PASSWORD" && step !== "COLLECT_PROFILE" && step !== "COLLECT_TOS" && step !== normalizedCompleteStep ? /* @__PURE__ */ jsxs("div", { className: classNames?.info, style: uiStyles.info, children: [
1031
+ !builtInDefaultSteps.has(String(step)) && step !== normalizedCompleteStep ? /* @__PURE__ */ jsxs("div", { className: classNames?.info, style: uiStyles.info, children: [
623
1032
  'No default renderer for step "',
624
1033
  step,
625
1034
  '". Provide renderStep to handle custom steps.'
@@ -631,6 +1040,7 @@ function AdaptiveFlow({
631
1040
  busy,
632
1041
  message,
633
1042
  errorMessage,
1043
+ fieldErrors,
634
1044
  missingRequirements: missing,
635
1045
  requirements: normalizedRequirements,
636
1046
  defaultView,
@@ -645,6 +1055,7 @@ function AdaptiveFlow({
645
1055
  busy,
646
1056
  message,
647
1057
  errorMessage,
1058
+ fieldErrors,
648
1059
  missingRequirements: missing,
649
1060
  requirements: normalizedRequirements,
650
1061
  setContextPatch: patchContext,
@@ -667,38 +1078,31 @@ function AdaptiveFlow({
667
1078
  message ? /* @__PURE__ */ jsx("div", { className: classNames?.success, style: uiStyles.success, children: message }) : null,
668
1079
  errorMessage ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: uiStyles.error, children: errorMessage }) : null,
669
1080
  customView ?? registryView ?? defaultView,
670
- /* @__PURE__ */ jsxs("div", { className: classNames?.footer, style: uiStyles.footer, children: [
671
- /* @__PURE__ */ jsx(
672
- "button",
673
- {
674
- type: "button",
675
- className: classNames?.oauthButton,
676
- style: uiStyles.oauthButton,
677
- disabled: busy || !adapter?.startOAuth,
678
- onClick: () => {
679
- handleOAuth("google");
680
- },
681
- children: "Continue with Google"
682
- }
683
- ),
684
- /* @__PURE__ */ jsx(
685
- "button",
686
- {
687
- type: "button",
688
- className: classNames?.oauthButton,
689
- style: uiStyles.oauthButton,
690
- disabled: busy || !adapter?.startOAuth,
691
- onClick: () => {
692
- handleOAuth("apple");
693
- },
694
- children: "Continue with Apple"
695
- }
696
- )
697
- ] })
1081
+ /* @__PURE__ */ jsx("div", { className: classNames?.footer, style: uiStyles.footer, children: normalizedOAuthProviders.map((provider) => /* @__PURE__ */ jsx(
1082
+ "button",
1083
+ {
1084
+ type: "button",
1085
+ className: classNames?.oauthButton,
1086
+ style: uiStyles.oauthButton,
1087
+ disabled: busy || !adapter?.startOAuth,
1088
+ onClick: () => {
1089
+ handleOAuth(provider.id);
1090
+ },
1091
+ children: provider.label
1092
+ },
1093
+ provider.id
1094
+ )) })
698
1095
  ] });
699
1096
  }
700
- function EmailBlock({ onSubmit, disabled, styles, classNames }) {
1097
+ function EmailBlock({
1098
+ onSubmit,
1099
+ disabled,
1100
+ error,
1101
+ styles,
1102
+ classNames
1103
+ }) {
701
1104
  const [email, setEmail] = React.useState("");
1105
+ const errorId = "adaptive-flow-email-error";
702
1106
  return /* @__PURE__ */ jsxs(
703
1107
  "form",
704
1108
  {
@@ -719,10 +1123,13 @@ function EmailBlock({ onSubmit, disabled, styles, classNames }) {
719
1123
  placeholder: "Enter your email",
720
1124
  value: email,
721
1125
  onChange: (event) => setEmail(event.target.value),
1126
+ "aria-invalid": Boolean(error),
1127
+ "aria-describedby": error ? errorId : void 0,
722
1128
  required: true
723
1129
  }
724
1130
  ),
725
- /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
1131
+ /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" }),
1132
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
726
1133
  ]
727
1134
  }
728
1135
  );
@@ -731,10 +1138,12 @@ function OtpBlock({
731
1138
  onSubmit,
732
1139
  disabled,
733
1140
  email,
1141
+ error,
734
1142
  styles,
735
1143
  classNames
736
1144
  }) {
737
1145
  const [code, setCode] = React.useState("");
1146
+ const errorId = "adaptive-flow-otp-error";
738
1147
  return /* @__PURE__ */ jsxs(
739
1148
  "form",
740
1149
  {
@@ -760,6 +1169,8 @@ function OtpBlock({
760
1169
  placeholder: "Enter 6-digit code",
761
1170
  value: code,
762
1171
  onChange: (event) => setCode(event.target.value.replace(/\D/g, "").slice(0, 6)),
1172
+ "aria-invalid": Boolean(error),
1173
+ "aria-describedby": error ? errorId : void 0,
763
1174
  required: true,
764
1175
  maxLength: 6,
765
1176
  pattern: "[0-9]{6}"
@@ -774,7 +1185,8 @@ function OtpBlock({
774
1185
  type: "submit",
775
1186
  children: "Verify"
776
1187
  }
777
- )
1188
+ ),
1189
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
778
1190
  ] })
779
1191
  ]
780
1192
  }
@@ -784,10 +1196,12 @@ function PasswordBlock({
784
1196
  onSubmit,
785
1197
  disabled,
786
1198
  hasPassword,
1199
+ error,
787
1200
  styles,
788
1201
  classNames
789
1202
  }) {
790
1203
  const [password, setPassword] = React.useState("");
1204
+ const errorId = "adaptive-flow-password-error";
791
1205
  return /* @__PURE__ */ jsxs(
792
1206
  "form",
793
1207
  {
@@ -808,11 +1222,14 @@ function PasswordBlock({
808
1222
  placeholder: hasPassword ? "Enter your password" : "Create a password",
809
1223
  value: password,
810
1224
  onChange: (event) => setPassword(event.target.value),
1225
+ "aria-invalid": Boolean(error),
1226
+ "aria-describedby": error ? errorId : void 0,
811
1227
  required: true,
812
1228
  minLength: 8
813
1229
  }
814
1230
  ),
815
- /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
1231
+ /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" }),
1232
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, className: classNames?.error, style: styles.error, children: error }) : null
816
1233
  ]
817
1234
  }
818
1235
  );
@@ -822,12 +1239,17 @@ function ProfileBlock({
822
1239
  disabled,
823
1240
  defaultValue,
824
1241
  requireJobTitle,
1242
+ errors,
825
1243
  styles,
826
1244
  classNames
827
1245
  }) {
828
1246
  const [firstName, setFirstName] = React.useState(defaultValue.firstName ?? "");
829
1247
  const [lastName, setLastName] = React.useState(defaultValue.lastName ?? "");
830
1248
  const [jobTitle, setJobTitle] = React.useState(defaultValue.jobTitle ?? "");
1249
+ const firstNameError = errors?.firstName ?? errors?.["profile.firstName"];
1250
+ const lastNameError = errors?.lastName ?? errors?.["profile.lastName"];
1251
+ const jobTitleError = errors?.jobTitle ?? errors?.["profile.jobTitle"];
1252
+ const profileError = errors?.profile;
831
1253
  return /* @__PURE__ */ jsxs(
832
1254
  "form",
833
1255
  {
@@ -853,6 +1275,7 @@ function ProfileBlock({
853
1275
  placeholder: "First name",
854
1276
  value: firstName,
855
1277
  onChange: (event) => setFirstName(event.target.value),
1278
+ "aria-invalid": Boolean(firstNameError),
856
1279
  required: true
857
1280
  }
858
1281
  ),
@@ -866,23 +1289,31 @@ function ProfileBlock({
866
1289
  placeholder: "Last name",
867
1290
  value: lastName,
868
1291
  onChange: (event) => setLastName(event.target.value),
1292
+ "aria-invalid": Boolean(lastNameError),
869
1293
  required: true
870
1294
  }
871
1295
  )
872
1296
  ] }),
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,
1297
+ firstNameError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: firstNameError }) : null,
1298
+ lastNameError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: lastNameError }) : null,
1299
+ requireJobTitle ? /* @__PURE__ */ jsxs(Fragment, { children: [
1300
+ /* @__PURE__ */ jsx(
1301
+ "input",
1302
+ {
1303
+ className: classNames?.input,
1304
+ style: styles.input,
1305
+ type: "text",
1306
+ autoComplete: "organization-title",
1307
+ placeholder: "Job title",
1308
+ value: jobTitle,
1309
+ onChange: (event) => setJobTitle(event.target.value),
1310
+ "aria-invalid": Boolean(jobTitleError),
1311
+ required: true
1312
+ }
1313
+ ),
1314
+ jobTitleError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: jobTitleError }) : null
1315
+ ] }) : null,
1316
+ profileError ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: profileError }) : null,
886
1317
  /* @__PURE__ */ jsx("button", { className: classNames?.button, style: styles.button, disabled, type: "submit", children: "Continue" })
887
1318
  ]
888
1319
  }
@@ -891,6 +1322,7 @@ function ProfileBlock({
891
1322
  function TosBlock({
892
1323
  onSubmit,
893
1324
  disabled,
1325
+ error,
894
1326
  styles,
895
1327
  classNames
896
1328
  }) {
@@ -908,9 +1340,18 @@ function TosBlock({
908
1340
  },
909
1341
  children: [
910
1342
  /* @__PURE__ */ jsxs("label", { className: classNames?.checkboxRow, style: styles.checkboxRow, children: [
911
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked, onChange: (event) => setChecked(event.target.checked) }),
1343
+ /* @__PURE__ */ jsx(
1344
+ "input",
1345
+ {
1346
+ type: "checkbox",
1347
+ checked,
1348
+ onChange: (event) => setChecked(event.target.checked),
1349
+ "aria-invalid": Boolean(error)
1350
+ }
1351
+ ),
912
1352
  /* @__PURE__ */ jsx("span", { children: "I agree to the Terms of Service and Privacy Policy." })
913
1353
  ] }),
1354
+ error ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: styles.error, children: error }) : null,
914
1355
  /* @__PURE__ */ jsx(
915
1356
  "button",
916
1357
  {
@@ -930,7 +1371,7 @@ function CompleteBlock({
930
1371
  classNames
931
1372
  }) {
932
1373
  return /* @__PURE__ */ jsxs("div", { className: classNames?.complete, style: styles.complete, children: [
933
- /* @__PURE__ */ jsx("strong", { children: "You are fully authenticated." }),
1374
+ /* @__PURE__ */ jsx("strong", { children: "You are all set." }),
934
1375
  /* @__PURE__ */ jsx("p", { className: classNames?.caption, style: styles.caption, children: "All requirements are complete." })
935
1376
  ] });
936
1377
  }
@@ -1068,6 +1509,7 @@ export {
1068
1509
  defaultRequirements,
1069
1510
  evaluateNextStep,
1070
1511
  getMissingRequirements,
1071
- initialContext
1512
+ initialContext,
1513
+ useAdaptiveFlow
1072
1514
  };
1073
1515
  //# sourceMappingURL=index.mjs.map