@secmia/openui-flow 4.1.0 → 4.2.1

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.js CHANGED
@@ -71,6 +71,8 @@ var defaultRequirements = [
71
71
  DefaultAppRequirements.EMAIL_VERIFIED,
72
72
  DefaultAppRequirements.HAS_PASSWORD,
73
73
  DefaultAppRequirements.HAS_FIRST_NAME,
74
+ DefaultAppRequirements.HAS_LAST_NAME,
75
+ DefaultAppRequirements.HAS_JOB_TITLE,
74
76
  DefaultAppRequirements.ACCEPTED_TOS
75
77
  ];
76
78
  var initialContext = {
@@ -116,20 +118,66 @@ var defaultRequirementResolvers = {
116
118
  };
117
119
  function createRequirementGraph(requirements, resolvers, options) {
118
120
  const graph = [];
121
+ const requirementSet = new Set(requirements);
122
+ const resolverBackedRequirements = new Set(
123
+ requirements.filter((requirement) => Boolean(resolvers[requirement]))
124
+ );
119
125
  for (const requirement of requirements) {
120
126
  const resolver = resolvers[requirement];
121
127
  if (!resolver) {
122
128
  continue;
123
129
  }
130
+ const dependsOn = options?.dependencies?.[requirement] ?? [];
131
+ for (const dependency of dependsOn) {
132
+ if (!requirementSet.has(dependency)) {
133
+ throw new Error(
134
+ `Invalid dependency "${String(dependency)}" referenced by requirement "${String(requirement)}".`
135
+ );
136
+ }
137
+ if (!resolverBackedRequirements.has(dependency)) {
138
+ throw new Error(
139
+ `Dependency "${String(dependency)}" referenced by requirement "${String(requirement)}" has no resolver.`
140
+ );
141
+ }
142
+ }
124
143
  graph.push({
125
144
  requirement,
126
145
  step: resolver.step,
127
146
  isMet: resolver.isMet,
128
147
  when: options?.conditions?.[requirement],
129
148
  priority: options?.priorities?.[requirement] ?? 0,
130
- dependsOn: options?.dependencies?.[requirement] ?? []
149
+ dependsOn
131
150
  });
132
151
  }
152
+ const byRequirement = new Map(
153
+ graph.map((node) => [node.requirement, node])
154
+ );
155
+ const visited = /* @__PURE__ */ new Set();
156
+ const visiting = /* @__PURE__ */ new Set();
157
+ const dfs = (requirement, stack) => {
158
+ if (visited.has(requirement)) {
159
+ return;
160
+ }
161
+ if (visiting.has(requirement)) {
162
+ const cycleStart = stack.indexOf(requirement);
163
+ const cyclePath = [...stack.slice(cycleStart), requirement].map((value) => String(value)).join(" -> ");
164
+ throw new Error(`Circular dependency detected in requirement graph: ${cyclePath}`);
165
+ }
166
+ visiting.add(requirement);
167
+ const node = byRequirement.get(requirement);
168
+ if (node?.dependsOn?.length) {
169
+ for (const dep of node.dependsOn) {
170
+ if (byRequirement.has(dep)) {
171
+ dfs(dep, [...stack, requirement]);
172
+ }
173
+ }
174
+ }
175
+ visiting.delete(requirement);
176
+ visited.add(requirement);
177
+ };
178
+ for (const node of graph) {
179
+ dfs(node.requirement, []);
180
+ }
133
181
  return graph;
134
182
  }
135
183
  function createDefaultRequirementGraph(options) {
@@ -145,13 +193,63 @@ function createDefaultRequirementGraph(options) {
145
193
  });
146
194
  }
147
195
  function sortGraph(graph) {
148
- return [...graph].sort((left, right) => {
149
- const byPriority = (right.priority ?? 0) - (left.priority ?? 0);
196
+ const entries = graph.map((node, index) => ({ node, index }));
197
+ const byRequirement = new Map(entries.map((entry) => [entry.node.requirement, entry]));
198
+ const indegree = /* @__PURE__ */ new Map();
199
+ const adjacency = /* @__PURE__ */ new Map();
200
+ for (const entry of entries) {
201
+ indegree.set(entry.node.requirement, 0);
202
+ adjacency.set(entry.node.requirement, []);
203
+ }
204
+ for (const entry of entries) {
205
+ const current = entry.node.requirement;
206
+ for (const dependency of entry.node.dependsOn ?? []) {
207
+ const depEntry = byRequirement.get(dependency);
208
+ if (!depEntry) {
209
+ continue;
210
+ }
211
+ const outgoing = adjacency.get(depEntry.node.requirement);
212
+ if (outgoing) {
213
+ outgoing.push(current);
214
+ }
215
+ indegree.set(current, (indegree.get(current) ?? 0) + 1);
216
+ }
217
+ }
218
+ const compare = (left, right) => {
219
+ const byPriority = (right.node.priority ?? 0) - (left.node.priority ?? 0);
150
220
  if (byPriority !== 0) {
151
221
  return byPriority;
152
222
  }
153
- return 0;
154
- });
223
+ const byRequirementName = String(left.node.requirement).localeCompare(String(right.node.requirement));
224
+ if (byRequirementName !== 0) {
225
+ return byRequirementName;
226
+ }
227
+ return left.index - right.index;
228
+ };
229
+ const ready = entries.filter((entry) => (indegree.get(entry.node.requirement) ?? 0) === 0).sort(compare);
230
+ const ordered = [];
231
+ while (ready.length > 0) {
232
+ const current = ready.shift();
233
+ if (!current) {
234
+ break;
235
+ }
236
+ ordered.push(current.node);
237
+ for (const dependent of adjacency.get(current.node.requirement) ?? []) {
238
+ const next = (indegree.get(dependent) ?? 0) - 1;
239
+ indegree.set(dependent, next);
240
+ if (next === 0) {
241
+ const dependentEntry = byRequirement.get(dependent);
242
+ if (dependentEntry) {
243
+ ready.push(dependentEntry);
244
+ ready.sort(compare);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ if (ordered.length !== graph.length) {
250
+ throw new Error("Unable to topologically sort requirement graph. Check for dependency cycles.");
251
+ }
252
+ return ordered;
155
253
  }
156
254
  async function evaluateNextStep(context, graph, completeStep) {
157
255
  for (const node of sortGraph(graph)) {
@@ -192,6 +290,10 @@ async function getMissingRequirements(context, graph) {
192
290
 
193
291
  // src/AdaptiveFlow.tsx
194
292
  var import_jsx_runtime = require("react/jsx-runtime");
293
+ var defaultOAuthProviders = [
294
+ { id: "google", label: "Continue with Google" },
295
+ { id: "apple", label: "Continue with Apple" }
296
+ ];
195
297
  var defaultStepTitle = {
196
298
  COLLECT_EMAIL: "Enter your email",
197
299
  VERIFY_OTP: "Verify your email",
@@ -200,6 +302,14 @@ var defaultStepTitle = {
200
302
  COLLECT_TOS: "Accept terms",
201
303
  COMPLETE: "Done"
202
304
  };
305
+ var builtInDefaultSteps = /* @__PURE__ */ new Set([
306
+ "COLLECT_EMAIL",
307
+ "VERIFY_OTP",
308
+ "COLLECT_PASSWORD",
309
+ "COLLECT_PROFILE",
310
+ "COLLECT_TOS",
311
+ "COMPLETE"
312
+ ]);
203
313
  var styleSlots = [
204
314
  "shell",
205
315
  "headerRow",
@@ -220,14 +330,24 @@ var styleSlots = [
220
330
  "oauthButton"
221
331
  ];
222
332
  function mergeContext(current, patch) {
223
- return {
224
- ...current,
225
- ...patch,
226
- profile: {
227
- ...current.profile,
228
- ...patch.profile
333
+ const mergeValue = (baseValue, patchValue) => {
334
+ if (Array.isArray(baseValue) || Array.isArray(patchValue)) {
335
+ return patchValue;
336
+ }
337
+ const baseIsObject = Boolean(baseValue) && typeof baseValue === "object";
338
+ const patchIsObject = Boolean(patchValue) && typeof patchValue === "object";
339
+ if (!baseIsObject || !patchIsObject) {
340
+ return patchValue === void 0 ? baseValue : patchValue;
341
+ }
342
+ const baseObject = baseValue;
343
+ const patchObject = patchValue;
344
+ const result = { ...baseObject };
345
+ for (const key of Object.keys(patchObject)) {
346
+ result[key] = mergeValue(baseObject[key], patchObject[key]);
229
347
  }
348
+ return result;
230
349
  };
350
+ return mergeValue(current, patch);
231
351
  }
232
352
  function withDefaults(initialValue) {
233
353
  if (!initialValue) {
@@ -239,7 +359,65 @@ function toError(error) {
239
359
  if (error instanceof Error) {
240
360
  return error;
241
361
  }
242
- return new Error("Unknown error while processing adaptive flow");
362
+ if (typeof error === "string") {
363
+ return new Error(error);
364
+ }
365
+ if (error && typeof error === "object") {
366
+ const maybeError = error;
367
+ const message = typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : "Unknown error while processing adaptive flow";
368
+ const normalized = new Error(message);
369
+ if (typeof maybeError.code === "string" && maybeError.code.trim().length > 0) {
370
+ normalized.name = maybeError.code;
371
+ }
372
+ normalized.cause = error;
373
+ return normalized;
374
+ }
375
+ return new Error(`Unknown error while processing adaptive flow: ${String(error)}`);
376
+ }
377
+ function sleep(ms) {
378
+ return new Promise((resolve) => {
379
+ setTimeout(resolve, ms);
380
+ });
381
+ }
382
+ function normalizeDelay(delay, fallback) {
383
+ if (typeof delay !== "number" || Number.isNaN(delay) || delay < 0) {
384
+ return fallback;
385
+ }
386
+ return delay;
387
+ }
388
+ function computeRetryDelay(policy, attempt) {
389
+ if (policy?.delay) {
390
+ return normalizeDelay(policy.delay(attempt), 0);
391
+ }
392
+ const initialDelayMs = normalizeDelay(policy?.initialDelayMs, 250);
393
+ const factor = typeof policy?.factor === "number" && policy.factor > 0 ? policy.factor : 2;
394
+ const maxDelayMs = normalizeDelay(policy?.maxDelayMs, Number.POSITIVE_INFINITY);
395
+ let delay = initialDelayMs * Math.pow(factor, Math.max(0, attempt - 1));
396
+ if (policy?.jitter) {
397
+ delay = delay * (0.5 + Math.random() * 0.5);
398
+ }
399
+ return Math.min(delay, maxDelayMs);
400
+ }
401
+ async function withRetry(operation, retryPolicy) {
402
+ if (!retryPolicy) {
403
+ return operation();
404
+ }
405
+ const maxAttempts = Math.max(1, Math.trunc(retryPolicy.maxAttempts ?? 3));
406
+ let lastError;
407
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
408
+ try {
409
+ return await operation();
410
+ } catch (error) {
411
+ lastError = error;
412
+ const normalized = toError(error);
413
+ const shouldRetry = retryPolicy.shouldRetry?.(normalized, attempt) ?? attempt < maxAttempts;
414
+ if (!shouldRetry || attempt === maxAttempts) {
415
+ throw normalized;
416
+ }
417
+ await sleep(computeRetryDelay(retryPolicy, attempt));
418
+ }
419
+ }
420
+ throw toError(lastError);
243
421
  }
244
422
  function cx(...names) {
245
423
  const value = names.filter(Boolean).join(" ").trim();
@@ -394,7 +572,9 @@ function useAdaptiveFlow({
394
572
  onStepTransition,
395
573
  persistence,
396
574
  validators,
397
- schemas
575
+ schemas,
576
+ oauthProviders,
577
+ retryPolicy
398
578
  }) {
399
579
  const normalizedRequirements = React.useMemo(
400
580
  () => requirements ?? defaultRequirements,
@@ -428,20 +608,68 @@ function useAdaptiveFlow({
428
608
  requirementGraphConfig?.dependencies
429
609
  ]
430
610
  );
611
+ const runtimeReducer = (state, action) => {
612
+ switch (action.type) {
613
+ case "evaluated":
614
+ return {
615
+ ...state,
616
+ step: action.step,
617
+ missingRequirements: action.missingRequirements,
618
+ transitions: [...state.transitions, action.transition].slice(-100)
619
+ };
620
+ case "set_busy":
621
+ return { ...state, busy: action.busy };
622
+ case "set_message":
623
+ return { ...state, message: action.message };
624
+ case "set_error":
625
+ return { ...state, errorMessage: action.errorMessage };
626
+ case "set_field_errors":
627
+ return { ...state, fieldErrors: action.fieldErrors };
628
+ case "start_job":
629
+ return { ...state, busy: true, errorMessage: null, fieldErrors: {} };
630
+ case "set_oauth_pending":
631
+ return { ...state, oauthPendingProvider: action.provider };
632
+ case "set_hydrated":
633
+ return { ...state, persistenceHydrated: action.hydrated };
634
+ default:
635
+ return state;
636
+ }
637
+ };
431
638
  const [context, setContext] = React.useState(() => withDefaults(initialValue));
432
- const [step, setStep] = React.useState(normalizedCompleteStep);
433
- const [missingRequirements, setMissingRequirements] = React.useState([]);
434
- const [transitions, setTransitions] = React.useState([]);
435
- const [busy, setBusy] = React.useState(false);
436
- const [message, setMessage] = React.useState(null);
437
- const [errorMessage, setErrorMessage] = React.useState(null);
438
- const [fieldErrors, setFieldErrors] = React.useState({});
439
- const [oauthPendingProvider, setOauthPendingProvider] = React.useState(null);
440
- const [persistenceHydrated, setPersistenceHydrated] = React.useState(!persistence);
639
+ const [runtime, dispatch] = React.useReducer(runtimeReducer, {
640
+ step: normalizedCompleteStep,
641
+ missingRequirements: [],
642
+ transitions: [],
643
+ busy: false,
644
+ message: null,
645
+ errorMessage: null,
646
+ fieldErrors: {},
647
+ oauthPendingProvider: null,
648
+ persistenceHydrated: !persistence
649
+ });
650
+ const {
651
+ step,
652
+ missingRequirements,
653
+ transitions,
654
+ busy,
655
+ message,
656
+ errorMessage,
657
+ fieldErrors,
658
+ oauthPendingProvider,
659
+ persistenceHydrated
660
+ } = runtime;
441
661
  const attemptByStepRef = React.useRef({});
442
662
  const previousStepRef = React.useRef(null);
443
663
  const evaluationRef = React.useRef(0);
444
664
  const completed = React.useRef(false);
665
+ const reportPersistenceError = React.useCallback(
666
+ (error, phase) => {
667
+ const normalized = toError(error);
668
+ persistence?.onError?.(normalized, phase);
669
+ onError?.(normalized);
670
+ },
671
+ [onError, persistence]
672
+ );
445
673
  React.useEffect(() => {
446
674
  if (!persistence) {
447
675
  return;
@@ -452,26 +680,24 @@ function useAdaptiveFlow({
452
680
  setContext(mergeContext(withDefaults(initialValue), persisted.context));
453
681
  }
454
682
  if (persisted?.oauthPendingProvider) {
455
- setOauthPendingProvider(persisted.oauthPendingProvider);
683
+ dispatch({ type: "set_oauth_pending", provider: persisted.oauthPendingProvider });
456
684
  }
457
- } catch {
685
+ } catch (error) {
686
+ reportPersistenceError(error, "read");
458
687
  } finally {
459
- setPersistenceHydrated(true);
688
+ dispatch({ type: "set_hydrated", hydrated: true });
460
689
  }
461
- }, [initialValue, persistence]);
690
+ }, [initialValue, persistence, reportPersistenceError]);
462
691
  React.useEffect(() => {
463
- let isCancelled = false;
464
692
  const currentEvaluation = ++evaluationRef.current;
465
693
  void (async () => {
466
694
  const [missing, next] = await Promise.all([
467
695
  getMissingRequirements(context, graph),
468
696
  evaluateNextStep(context, graph, normalizedCompleteStep)
469
697
  ]);
470
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
698
+ if (currentEvaluation !== evaluationRef.current) {
471
699
  return;
472
700
  }
473
- setMissingRequirements(missing);
474
- setStep(next);
475
701
  const from = previousStepRef.current;
476
702
  const attemptKey = String(next);
477
703
  const nextAttempt = from === next ? (attemptByStepRef.current[attemptKey] ?? 0) + 1 : 1;
@@ -482,21 +708,23 @@ function useAdaptiveFlow({
482
708
  at: Date.now(),
483
709
  attempt: nextAttempt
484
710
  };
485
- setTransitions((previous) => [...previous, transition].slice(-100));
711
+ dispatch({
712
+ type: "evaluated",
713
+ missingRequirements: missing,
714
+ step: next,
715
+ transition
716
+ });
486
717
  previousStepRef.current = next;
487
718
  onStepTransition?.(transition, context);
488
719
  })().catch((error) => {
489
- if (isCancelled || currentEvaluation !== evaluationRef.current) {
720
+ if (currentEvaluation !== evaluationRef.current) {
490
721
  return;
491
722
  }
492
723
  const normalized = toError(error);
493
- setFieldErrors({});
494
- setErrorMessage(normalized.message);
724
+ dispatch({ type: "set_field_errors", fieldErrors: {} });
725
+ dispatch({ type: "set_error", errorMessage: normalized.message });
495
726
  onError?.(normalized);
496
727
  });
497
- return () => {
498
- isCancelled = true;
499
- };
500
728
  }, [context, graph, normalizedCompleteStep, onError, onStepTransition]);
501
729
  React.useEffect(() => {
502
730
  if (step === normalizedCompleteStep) {
@@ -505,43 +733,46 @@ function useAdaptiveFlow({
505
733
  onComplete?.(context);
506
734
  const shouldClearPersistence = persistence?.clearOnComplete ?? true;
507
735
  if (shouldClearPersistence) {
508
- clearPersistedState(persistence);
736
+ try {
737
+ clearPersistedState(persistence);
738
+ } catch (error) {
739
+ reportPersistenceError(error, "clear");
740
+ }
509
741
  }
510
742
  }
511
743
  } else {
512
744
  completed.current = false;
513
745
  }
514
- }, [context, normalizedCompleteStep, onComplete, persistence, step]);
746
+ }, [context, normalizedCompleteStep, onComplete, persistence, reportPersistenceError, step]);
515
747
  React.useEffect(() => {
516
748
  if (!persistence || !persistenceHydrated) {
517
749
  return;
518
750
  }
519
751
  try {
520
752
  writePersistedState(persistence, { context, oauthPendingProvider });
521
- } catch {
753
+ } catch (error) {
754
+ reportPersistenceError(error, "write");
522
755
  }
523
- }, [context, oauthPendingProvider, persistence, persistenceHydrated]);
756
+ }, [context, oauthPendingProvider, persistence, persistenceHydrated, reportPersistenceError]);
524
757
  const run = React.useCallback(
525
758
  async (job) => {
526
- setBusy(true);
527
- setErrorMessage(null);
528
- setFieldErrors({});
759
+ dispatch({ type: "start_job" });
529
760
  try {
530
761
  await job();
531
762
  } catch (error) {
532
763
  if (error instanceof FlowValidationError) {
533
- setFieldErrors(error.fieldErrors);
764
+ dispatch({ type: "set_field_errors", fieldErrors: error.fieldErrors });
534
765
  if (Object.keys(error.fieldErrors).length === 0) {
535
- setErrorMessage(error.message);
766
+ dispatch({ type: "set_error", errorMessage: error.message });
536
767
  onError?.(error);
537
768
  }
538
769
  } else {
539
770
  const normalized = toError(error);
540
- setErrorMessage(normalized.message);
771
+ dispatch({ type: "set_error", errorMessage: normalized.message });
541
772
  onError?.(normalized);
542
773
  }
543
774
  } finally {
544
- setBusy(false);
775
+ dispatch({ type: "set_busy", busy: false });
545
776
  }
546
777
  },
547
778
  [onError]
@@ -549,26 +780,23 @@ function useAdaptiveFlow({
549
780
  const patchContext = React.useCallback((patch) => {
550
781
  setContext((prev) => mergeContext(prev, patch));
551
782
  }, []);
552
- const patchBaseContext = React.useCallback(
553
- (patch) => {
554
- patchContext(patch);
555
- },
556
- [patchContext]
557
- );
558
783
  React.useEffect(() => {
559
784
  const completeOAuth = adapter?.completeOAuth;
560
785
  if (!oauthPendingProvider || !completeOAuth) {
561
786
  return;
562
787
  }
563
788
  void run(async () => {
564
- const patch = await completeOAuth(oauthPendingProvider, context);
789
+ const patch = await withRetry(
790
+ () => completeOAuth(oauthPendingProvider, context),
791
+ retryPolicy
792
+ );
565
793
  if (patch) {
566
794
  patchContext(patch);
567
795
  }
568
- setOauthPendingProvider(null);
569
- setMessage("OAuth sign-in completed.");
796
+ dispatch({ type: "set_oauth_pending", provider: null });
797
+ dispatch({ type: "set_message", message: "OAuth sign-in completed." });
570
798
  });
571
- }, [adapter, context, oauthPendingProvider, patchContext, run]);
799
+ }, [adapter, context, oauthPendingProvider, patchContext, retryPolicy, run]);
572
800
  const handleEmail = (emailInput) => {
573
801
  const email = emailInput.trim().toLowerCase();
574
802
  if (!email) {
@@ -579,8 +807,8 @@ function useAdaptiveFlow({
579
807
  if (validators?.email) {
580
808
  await assertValid(validators.email(email, { context }), "email");
581
809
  }
582
- const identity = await adapter?.lookupByEmail?.(email) ?? null;
583
- patchBaseContext({
810
+ const identity = adapter?.lookupByEmail ? await withRetry(() => adapter.lookupByEmail(email), retryPolicy) : null;
811
+ patchContext({
584
812
  email,
585
813
  hasPassword: Boolean(identity?.hasPassword),
586
814
  isVerified: Boolean(identity?.isVerified),
@@ -592,15 +820,23 @@ function useAdaptiveFlow({
592
820
  }
593
821
  });
594
822
  if (identity?.accountExists && identity.hasPassword) {
595
- setMessage("Welcome back. Enter your password to continue.");
823
+ dispatch({ type: "set_message", message: "Welcome back. Enter your password to continue." });
596
824
  return;
597
825
  }
598
826
  if (adapter?.requestOtp) {
599
- await adapter.requestOtp(email);
600
- setMessage("We sent a 6-digit code to your inbox.");
827
+ await withRetry(() => adapter.requestOtp(email), retryPolicy);
828
+ dispatch({ type: "set_message", message: "We sent a 6-digit code to your inbox." });
601
829
  } else {
602
- patchBaseContext({ isVerified: true });
603
- setMessage("No OTP adapter configured. Email verification was skipped.");
830
+ const env = globalThis.process?.env?.NODE_ENV;
831
+ const isDev = env !== "production";
832
+ if (!isDev) {
833
+ throw new Error("OTP adapter is required in production. Provide adapter.requestOtp.");
834
+ }
835
+ patchContext({ isVerified: true });
836
+ dispatch({
837
+ type: "set_message",
838
+ message: "No OTP adapter configured. Verification was skipped in development mode."
839
+ });
604
840
  }
605
841
  });
606
842
  };
@@ -614,10 +850,10 @@ function useAdaptiveFlow({
614
850
  await assertValid(validators.otp(code, { context, email: context.email }), "otp");
615
851
  }
616
852
  if (adapter?.verifyOtp) {
617
- await adapter.verifyOtp(context.email, code);
853
+ await withRetry(() => adapter.verifyOtp(context.email, code), retryPolicy);
618
854
  }
619
- patchBaseContext({ isVerified: true });
620
- setMessage("Email verified.");
855
+ patchContext({ isVerified: true });
856
+ dispatch({ type: "set_message", message: "Email verified." });
621
857
  });
622
858
  };
623
859
  const handlePassword = (password) => {
@@ -634,15 +870,18 @@ function useAdaptiveFlow({
634
870
  }
635
871
  if (context.hasPassword) {
636
872
  if (adapter?.signInWithPassword) {
637
- await adapter.signInWithPassword(context.email, password);
873
+ await withRetry(
874
+ () => adapter.signInWithPassword(context.email, password),
875
+ retryPolicy
876
+ );
638
877
  }
639
878
  } else {
640
879
  if (adapter?.createPassword) {
641
- await adapter.createPassword(password);
880
+ await withRetry(() => adapter.createPassword(password), retryPolicy);
642
881
  }
643
- patchBaseContext({ hasPassword: true });
882
+ patchContext({ hasPassword: true });
644
883
  }
645
- setMessage("Password step complete.");
884
+ dispatch({ type: "set_message", message: "Password step complete." });
646
885
  });
647
886
  };
648
887
  const handleProfile = (profile) => {
@@ -659,10 +898,10 @@ function useAdaptiveFlow({
659
898
  }
660
899
  });
661
900
  if (adapter?.saveProfile) {
662
- await adapter.saveProfile(next);
901
+ await withRetry(() => adapter.saveProfile(next), retryPolicy);
663
902
  }
664
903
  patchContext({ profile: next.profile });
665
- setMessage("Profile saved.");
904
+ dispatch({ type: "set_message", message: "Profile saved." });
666
905
  });
667
906
  };
668
907
  const handleTos = () => {
@@ -673,10 +912,10 @@ function useAdaptiveFlow({
673
912
  }
674
913
  const next = mergeContext(context, { agreedToTos: true });
675
914
  if (adapter?.acceptTos) {
676
- await adapter.acceptTos(next);
915
+ await withRetry(() => adapter.acceptTos(next), retryPolicy);
677
916
  }
678
- patchBaseContext({ agreedToTos: true });
679
- setMessage("Terms accepted.");
917
+ patchContext({ agreedToTos: true });
918
+ dispatch({ type: "set_message", message: "Terms accepted." });
680
919
  });
681
920
  };
682
921
  const handleOAuth = (provider) => {
@@ -685,9 +924,9 @@ function useAdaptiveFlow({
685
924
  return;
686
925
  }
687
926
  void run(async () => {
688
- setOauthPendingProvider(provider);
689
- setMessage(`Starting ${provider} sign-in...`);
690
- await startOAuth(provider, context);
927
+ dispatch({ type: "set_oauth_pending", provider });
928
+ dispatch({ type: "set_message", message: `Starting ${provider} sign-in...` });
929
+ await withRetry(() => startOAuth(provider, context), retryPolicy);
691
930
  });
692
931
  };
693
932
  return {
@@ -731,7 +970,9 @@ function AdaptiveFlow({
731
970
  unstyled = false,
732
971
  persistence,
733
972
  validators,
734
- schemas
973
+ schemas,
974
+ oauthProviders,
975
+ retryPolicy
735
976
  }) {
736
977
  const uiStyles = React.useMemo(() => resolveStyles(unstyled, styles), [unstyled, styles]);
737
978
  const {
@@ -766,8 +1007,14 @@ function AdaptiveFlow({
766
1007
  onStepTransition,
767
1008
  persistence,
768
1009
  validators,
769
- schemas
1010
+ schemas,
1011
+ oauthProviders,
1012
+ retryPolicy
770
1013
  });
1014
+ const normalizedOAuthProviders = React.useMemo(
1015
+ () => oauthProviders && oauthProviders.length > 0 ? oauthProviders : defaultOAuthProviders,
1016
+ [oauthProviders]
1017
+ );
771
1018
  const needsJobTitle = normalizedRequirements.includes("has_job_title");
772
1019
  const stepLabel = stepTitles?.[step] ?? defaultStepTitle[step] ?? step;
773
1020
  const defaultView = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -826,7 +1073,7 @@ function AdaptiveFlow({
826
1073
  }
827
1074
  ) : null,
828
1075
  step === normalizedCompleteStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompleteBlock, { styles: uiStyles, classNames }) : null,
829
- step !== "COLLECT_EMAIL" && step !== "VERIFY_OTP" && step !== "COLLECT_PASSWORD" && step !== "COLLECT_PROFILE" && step !== "COLLECT_TOS" && step !== normalizedCompleteStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: classNames?.info, style: uiStyles.info, children: [
1076
+ !builtInDefaultSteps.has(String(step)) && step !== normalizedCompleteStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: classNames?.info, style: uiStyles.info, children: [
830
1077
  'No default renderer for step "',
831
1078
  step,
832
1079
  '". Provide renderStep to handle custom steps.'
@@ -876,34 +1123,20 @@ function AdaptiveFlow({
876
1123
  message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: classNames?.success, style: uiStyles.success, children: message }) : null,
877
1124
  errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: classNames?.error, style: uiStyles.error, children: errorMessage }) : null,
878
1125
  customView ?? registryView ?? defaultView,
879
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: classNames?.footer, style: uiStyles.footer, children: [
880
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
881
- "button",
882
- {
883
- type: "button",
884
- className: classNames?.oauthButton,
885
- style: uiStyles.oauthButton,
886
- disabled: busy || !adapter?.startOAuth,
887
- onClick: () => {
888
- handleOAuth("google");
889
- },
890
- children: "Continue with Google"
891
- }
892
- ),
893
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
894
- "button",
895
- {
896
- type: "button",
897
- className: classNames?.oauthButton,
898
- style: uiStyles.oauthButton,
899
- disabled: busy || !adapter?.startOAuth,
900
- onClick: () => {
901
- handleOAuth("apple");
902
- },
903
- children: "Continue with Apple"
904
- }
905
- )
906
- ] })
1126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: classNames?.footer, style: uiStyles.footer, children: normalizedOAuthProviders.map((provider) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1127
+ "button",
1128
+ {
1129
+ type: "button",
1130
+ className: classNames?.oauthButton,
1131
+ style: uiStyles.oauthButton,
1132
+ disabled: busy || !adapter?.startOAuth,
1133
+ onClick: () => {
1134
+ handleOAuth(provider.id);
1135
+ },
1136
+ children: provider.label
1137
+ },
1138
+ provider.id
1139
+ )) })
907
1140
  ] });
908
1141
  }
909
1142
  function EmailBlock({