@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.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
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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 [
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
683
|
+
dispatch({ type: "set_oauth_pending", provider: persisted.oauthPendingProvider });
|
|
456
684
|
}
|
|
457
|
-
} catch {
|
|
685
|
+
} catch (error) {
|
|
686
|
+
reportPersistenceError(error, "read");
|
|
458
687
|
} finally {
|
|
459
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
720
|
+
if (currentEvaluation !== evaluationRef.current) {
|
|
490
721
|
return;
|
|
491
722
|
}
|
|
492
723
|
const normalized = toError(error);
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
764
|
+
dispatch({ type: "set_field_errors", fieldErrors: error.fieldErrors });
|
|
534
765
|
if (Object.keys(error.fieldErrors).length === 0) {
|
|
535
|
-
|
|
766
|
+
dispatch({ type: "set_error", errorMessage: error.message });
|
|
536
767
|
onError?.(error);
|
|
537
768
|
}
|
|
538
769
|
} else {
|
|
539
770
|
const normalized = toError(error);
|
|
540
|
-
|
|
771
|
+
dispatch({ type: "set_error", errorMessage: normalized.message });
|
|
541
772
|
onError?.(normalized);
|
|
542
773
|
}
|
|
543
774
|
} finally {
|
|
544
|
-
|
|
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
|
|
789
|
+
const patch = await withRetry(
|
|
790
|
+
() => completeOAuth(oauthPendingProvider, context),
|
|
791
|
+
retryPolicy
|
|
792
|
+
);
|
|
565
793
|
if (patch) {
|
|
566
794
|
patchContext(patch);
|
|
567
795
|
}
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
|
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
|
-
|
|
882
|
+
patchContext({ hasPassword: true });
|
|
644
883
|
}
|
|
645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
|
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.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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({
|