@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/AGENTS.md +19 -0
- package/README.md +89 -0
- package/dist/index.d.mts +86 -4
- package/dist/index.d.ts +86 -4
- package/dist/index.js +344 -111
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -111
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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 [
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
638
|
+
dispatch({ type: "set_oauth_pending", provider: persisted.oauthPendingProvider });
|
|
411
639
|
}
|
|
412
|
-
} catch {
|
|
640
|
+
} catch (error) {
|
|
641
|
+
reportPersistenceError(error, "read");
|
|
413
642
|
} finally {
|
|
414
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
675
|
+
if (currentEvaluation !== evaluationRef.current) {
|
|
445
676
|
return;
|
|
446
677
|
}
|
|
447
678
|
const normalized = toError(error);
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
+
dispatch({ type: "set_field_errors", fieldErrors: error.fieldErrors });
|
|
489
720
|
if (Object.keys(error.fieldErrors).length === 0) {
|
|
490
|
-
|
|
721
|
+
dispatch({ type: "set_error", errorMessage: error.message });
|
|
491
722
|
onError?.(error);
|
|
492
723
|
}
|
|
493
724
|
} else {
|
|
494
725
|
const normalized = toError(error);
|
|
495
|
-
|
|
726
|
+
dispatch({ type: "set_error", errorMessage: normalized.message });
|
|
496
727
|
onError?.(normalized);
|
|
497
728
|
}
|
|
498
729
|
} finally {
|
|
499
|
-
|
|
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
|
|
744
|
+
const patch = await withRetry(
|
|
745
|
+
() => completeOAuth(oauthPendingProvider, context),
|
|
746
|
+
retryPolicy
|
|
747
|
+
);
|
|
520
748
|
if (patch) {
|
|
521
749
|
patchContext(patch);
|
|
522
750
|
}
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
|
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
|
-
|
|
837
|
+
patchContext({ hasPassword: true });
|
|
599
838
|
}
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
|
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__ */
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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({
|