@secmia/openui-flow 4.1.0 → 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;
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]);
184
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();
@@ -349,7 +527,9 @@ function useAdaptiveFlow({
349
527
  onStepTransition,
350
528
  persistence,
351
529
  validators,
352
- schemas
530
+ schemas,
531
+ oauthProviders,
532
+ retryPolicy
353
533
  }) {
354
534
  const normalizedRequirements = React.useMemo(
355
535
  () => requirements ?? defaultRequirements,
@@ -383,20 +563,68 @@ function useAdaptiveFlow({
383
563
  requirementGraphConfig?.dependencies
384
564
  ]
385
565
  );
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
+ };
386
593
  const [context, setContext] = React.useState(() => withDefaults(initialValue));
387
- const [step, setStep] = React.useState(normalizedCompleteStep);
388
- const [missingRequirements, setMissingRequirements] = React.useState([]);
389
- const [transitions, setTransitions] = React.useState([]);
390
- const [busy, setBusy] = React.useState(false);
391
- const [message, setMessage] = React.useState(null);
392
- const [errorMessage, setErrorMessage] = React.useState(null);
393
- const [fieldErrors, setFieldErrors] = React.useState({});
394
- const [oauthPendingProvider, setOauthPendingProvider] = React.useState(null);
395
- 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;
396
616
  const attemptByStepRef = React.useRef({});
397
617
  const previousStepRef = React.useRef(null);
398
618
  const evaluationRef = React.useRef(0);
399
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
+ );
400
628
  React.useEffect(() => {
401
629
  if (!persistence) {
402
630
  return;
@@ -407,26 +635,24 @@ function useAdaptiveFlow({
407
635
  setContext(mergeContext(withDefaults(initialValue), persisted.context));
408
636
  }
409
637
  if (persisted?.oauthPendingProvider) {
410
- setOauthPendingProvider(persisted.oauthPendingProvider);
638
+ dispatch({ type: "set_oauth_pending", provider: persisted.oauthPendingProvider });
411
639
  }
412
- } catch {
640
+ } catch (error) {
641
+ reportPersistenceError(error, "read");
413
642
  } finally {
414
- setPersistenceHydrated(true);
643
+ dispatch({ type: "set_hydrated", hydrated: true });
415
644
  }
416
- }, [initialValue, persistence]);
645
+ }, [initialValue, persistence, reportPersistenceError]);
417
646
  React.useEffect(() => {
418
- let isCancelled = false;
419
647
  const currentEvaluation = ++evaluationRef.current;
420
648
  void (async () => {
421
649
  const [missing, next] = await Promise.all([
422
650
  getMissingRequirements(context, graph),
423
651
  evaluateNextStep(context, graph, normalizedCompleteStep)
424
652
  ]);
425
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
653
+ if (currentEvaluation !== evaluationRef.current) {
426
654
  return;
427
655
  }
428
- setMissingRequirements(missing);
429
- setStep(next);
430
656
  const from = previousStepRef.current;
431
657
  const attemptKey = String(next);
432
658
  const nextAttempt = from === next ? (attemptByStepRef.current[attemptKey] ?? 0) + 1 : 1;
@@ -437,21 +663,23 @@ function useAdaptiveFlow({
437
663
  at: Date.now(),
438
664
  attempt: nextAttempt
439
665
  };
440
- setTransitions((previous) => [...previous, transition].slice(-100));
666
+ dispatch({
667
+ type: "evaluated",
668
+ missingRequirements: missing,
669
+ step: next,
670
+ transition
671
+ });
441
672
  previousStepRef.current = next;
442
673
  onStepTransition?.(transition, context);
443
674
  })().catch((error) => {
444
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
675
+ if (currentEvaluation !== evaluationRef.current) {
445
676
  return;
446
677
  }
447
678
  const normalized = toError(error);
448
- setFieldErrors({});
449
- setErrorMessage(normalized.message);
679
+ dispatch({ type: "set_field_errors", fieldErrors: {} });
680
+ dispatch({ type: "set_error", errorMessage: normalized.message });
450
681
  onError?.(normalized);
451
682
  });
452
- return () => {
453
- isCancelled = true;
454
- };
455
683
  }, [context, graph, normalizedCompleteStep, onError, onStepTransition]);
456
684
  React.useEffect(() => {
457
685
  if (step === normalizedCompleteStep) {
@@ -460,43 +688,46 @@ function useAdaptiveFlow({
460
688
  onComplete?.(context);
461
689
  const shouldClearPersistence = persistence?.clearOnComplete ?? true;
462
690
  if (shouldClearPersistence) {
463
- clearPersistedState(persistence);
691
+ try {
692
+ clearPersistedState(persistence);
693
+ } catch (error) {
694
+ reportPersistenceError(error, "clear");
695
+ }
464
696
  }
465
697
  }
466
698
  } else {
467
699
  completed.current = false;
468
700
  }
469
- }, [context, normalizedCompleteStep, onComplete, persistence, step]);
701
+ }, [context, normalizedCompleteStep, onComplete, persistence, reportPersistenceError, step]);
470
702
  React.useEffect(() => {
471
703
  if (!persistence || !persistenceHydrated) {
472
704
  return;
473
705
  }
474
706
  try {
475
707
  writePersistedState(persistence, { context, oauthPendingProvider });
476
- } catch {
708
+ } catch (error) {
709
+ reportPersistenceError(error, "write");
477
710
  }
478
- }, [context, oauthPendingProvider, persistence, persistenceHydrated]);
711
+ }, [context, oauthPendingProvider, persistence, persistenceHydrated, reportPersistenceError]);
479
712
  const run = React.useCallback(
480
713
  async (job) => {
481
- setBusy(true);
482
- setErrorMessage(null);
483
- setFieldErrors({});
714
+ dispatch({ type: "start_job" });
484
715
  try {
485
716
  await job();
486
717
  } catch (error) {
487
718
  if (error instanceof FlowValidationError) {
488
- setFieldErrors(error.fieldErrors);
719
+ dispatch({ type: "set_field_errors", fieldErrors: error.fieldErrors });
489
720
  if (Object.keys(error.fieldErrors).length === 0) {
490
- setErrorMessage(error.message);
721
+ dispatch({ type: "set_error", errorMessage: error.message });
491
722
  onError?.(error);
492
723
  }
493
724
  } else {
494
725
  const normalized = toError(error);
495
- setErrorMessage(normalized.message);
726
+ dispatch({ type: "set_error", errorMessage: normalized.message });
496
727
  onError?.(normalized);
497
728
  }
498
729
  } finally {
499
- setBusy(false);
730
+ dispatch({ type: "set_busy", busy: false });
500
731
  }
501
732
  },
502
733
  [onError]
@@ -504,26 +735,23 @@ function useAdaptiveFlow({
504
735
  const patchContext = React.useCallback((patch) => {
505
736
  setContext((prev) => mergeContext(prev, patch));
506
737
  }, []);
507
- const patchBaseContext = React.useCallback(
508
- (patch) => {
509
- patchContext(patch);
510
- },
511
- [patchContext]
512
- );
513
738
  React.useEffect(() => {
514
739
  const completeOAuth = adapter?.completeOAuth;
515
740
  if (!oauthPendingProvider || !completeOAuth) {
516
741
  return;
517
742
  }
518
743
  void run(async () => {
519
- const patch = await completeOAuth(oauthPendingProvider, context);
744
+ const patch = await withRetry(
745
+ () => completeOAuth(oauthPendingProvider, context),
746
+ retryPolicy
747
+ );
520
748
  if (patch) {
521
749
  patchContext(patch);
522
750
  }
523
- setOauthPendingProvider(null);
524
- setMessage("OAuth sign-in completed.");
751
+ dispatch({ type: "set_oauth_pending", provider: null });
752
+ dispatch({ type: "set_message", message: "OAuth sign-in completed." });
525
753
  });
526
- }, [adapter, context, oauthPendingProvider, patchContext, run]);
754
+ }, [adapter, context, oauthPendingProvider, patchContext, retryPolicy, run]);
527
755
  const handleEmail = (emailInput) => {
528
756
  const email = emailInput.trim().toLowerCase();
529
757
  if (!email) {
@@ -534,8 +762,8 @@ function useAdaptiveFlow({
534
762
  if (validators?.email) {
535
763
  await assertValid(validators.email(email, { context }), "email");
536
764
  }
537
- const identity = await adapter?.lookupByEmail?.(email) ?? null;
538
- patchBaseContext({
765
+ const identity = adapter?.lookupByEmail ? await withRetry(() => adapter.lookupByEmail(email), retryPolicy) : null;
766
+ patchContext({
539
767
  email,
540
768
  hasPassword: Boolean(identity?.hasPassword),
541
769
  isVerified: Boolean(identity?.isVerified),
@@ -547,15 +775,23 @@ function useAdaptiveFlow({
547
775
  }
548
776
  });
549
777
  if (identity?.accountExists && identity.hasPassword) {
550
- setMessage("Welcome back. Enter your password to continue.");
778
+ dispatch({ type: "set_message", message: "Welcome back. Enter your password to continue." });
551
779
  return;
552
780
  }
553
781
  if (adapter?.requestOtp) {
554
- await adapter.requestOtp(email);
555
- 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." });
556
784
  } else {
557
- patchBaseContext({ isVerified: true });
558
- 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
+ });
559
795
  }
560
796
  });
561
797
  };
@@ -569,10 +805,10 @@ function useAdaptiveFlow({
569
805
  await assertValid(validators.otp(code, { context, email: context.email }), "otp");
570
806
  }
571
807
  if (adapter?.verifyOtp) {
572
- await adapter.verifyOtp(context.email, code);
808
+ await withRetry(() => adapter.verifyOtp(context.email, code), retryPolicy);
573
809
  }
574
- patchBaseContext({ isVerified: true });
575
- setMessage("Email verified.");
810
+ patchContext({ isVerified: true });
811
+ dispatch({ type: "set_message", message: "Email verified." });
576
812
  });
577
813
  };
578
814
  const handlePassword = (password) => {
@@ -589,15 +825,18 @@ function useAdaptiveFlow({
589
825
  }
590
826
  if (context.hasPassword) {
591
827
  if (adapter?.signInWithPassword) {
592
- await adapter.signInWithPassword(context.email, password);
828
+ await withRetry(
829
+ () => adapter.signInWithPassword(context.email, password),
830
+ retryPolicy
831
+ );
593
832
  }
594
833
  } else {
595
834
  if (adapter?.createPassword) {
596
- await adapter.createPassword(password);
835
+ await withRetry(() => adapter.createPassword(password), retryPolicy);
597
836
  }
598
- patchBaseContext({ hasPassword: true });
837
+ patchContext({ hasPassword: true });
599
838
  }
600
- setMessage("Password step complete.");
839
+ dispatch({ type: "set_message", message: "Password step complete." });
601
840
  });
602
841
  };
603
842
  const handleProfile = (profile) => {
@@ -614,10 +853,10 @@ function useAdaptiveFlow({
614
853
  }
615
854
  });
616
855
  if (adapter?.saveProfile) {
617
- await adapter.saveProfile(next);
856
+ await withRetry(() => adapter.saveProfile(next), retryPolicy);
618
857
  }
619
858
  patchContext({ profile: next.profile });
620
- setMessage("Profile saved.");
859
+ dispatch({ type: "set_message", message: "Profile saved." });
621
860
  });
622
861
  };
623
862
  const handleTos = () => {
@@ -628,10 +867,10 @@ function useAdaptiveFlow({
628
867
  }
629
868
  const next = mergeContext(context, { agreedToTos: true });
630
869
  if (adapter?.acceptTos) {
631
- await adapter.acceptTos(next);
870
+ await withRetry(() => adapter.acceptTos(next), retryPolicy);
632
871
  }
633
- patchBaseContext({ agreedToTos: true });
634
- setMessage("Terms accepted.");
872
+ patchContext({ agreedToTos: true });
873
+ dispatch({ type: "set_message", message: "Terms accepted." });
635
874
  });
636
875
  };
637
876
  const handleOAuth = (provider) => {
@@ -640,9 +879,9 @@ function useAdaptiveFlow({
640
879
  return;
641
880
  }
642
881
  void run(async () => {
643
- setOauthPendingProvider(provider);
644
- setMessage(`Starting ${provider} sign-in...`);
645
- 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);
646
885
  });
647
886
  };
648
887
  return {
@@ -686,7 +925,9 @@ function AdaptiveFlow({
686
925
  unstyled = false,
687
926
  persistence,
688
927
  validators,
689
- schemas
928
+ schemas,
929
+ oauthProviders,
930
+ retryPolicy
690
931
  }) {
691
932
  const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
692
933
  const {
@@ -721,8 +962,14 @@ function AdaptiveFlow({
721
962
  onStepTransition,
722
963
  persistence,
723
964
  validators,
724
- schemas
965
+ schemas,
966
+ oauthProviders,
967
+ retryPolicy
725
968
  });
969
+ const normalizedOAuthProviders = React.useMemo(
970
+ () => oauthProviders && oauthProviders.length > 0 ? oauthProviders : defaultOAuthProviders,
971
+ [oauthProviders]
972
+ );
726
973
  const needsJobTitle = normalizedRequirements.includes("has_job_title");
727
974
  const stepLabel = stepTitles?.[step] ?? defaultStepTitle[step] ?? step;
728
975
  const defaultView = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -781,7 +1028,7 @@ function AdaptiveFlow({
781
1028
  }
782
1029
  ) : null,
783
1030
  step === normalizedCompleteStep ? /* @__PURE__ */ jsx(CompleteBlock, { styles: uiStyles, classNames }) : null,
784
- 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: [
785
1032
  'No default renderer for step "',
786
1033
  step,
787
1034
  '". Provide renderStep to handle custom steps.'
@@ -831,34 +1078,20 @@ function AdaptiveFlow({
831
1078
  message ? /* @__PURE__ */ jsx("div", { className: classNames?.success, style: uiStyles.success, children: message }) : null,
832
1079
  errorMessage ? /* @__PURE__ */ jsx("div", { className: classNames?.error, style: uiStyles.error, children: errorMessage }) : null,
833
1080
  customView ?? registryView ?? defaultView,
834
- /* @__PURE__ */ jsxs("div", { className: classNames?.footer, style: uiStyles.footer, children: [
835
- /* @__PURE__ */ jsx(
836
- "button",
837
- {
838
- type: "button",
839
- className: classNames?.oauthButton,
840
- style: uiStyles.oauthButton,
841
- disabled: busy || !adapter?.startOAuth,
842
- onClick: () => {
843
- handleOAuth("google");
844
- },
845
- children: "Continue with Google"
846
- }
847
- ),
848
- /* @__PURE__ */ jsx(
849
- "button",
850
- {
851
- type: "button",
852
- className: classNames?.oauthButton,
853
- style: uiStyles.oauthButton,
854
- disabled: busy || !adapter?.startOAuth,
855
- onClick: () => {
856
- handleOAuth("apple");
857
- },
858
- children: "Continue with Apple"
859
- }
860
- )
861
- ] })
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
+ )) })
862
1095
  ] });
863
1096
  }
864
1097
  function EmailBlock({