@secmia/openui-flow 3.0.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/README.md ADDED
@@ -0,0 +1,804 @@
1
+ # @secmia/openui-flow
2
+
3
+ Production-ready adaptive workflow and onboarding flow engine for React.
4
+
5
+ If you are evaluating quickly, you can get first success in under 3 minutes.
6
+
7
+ ## 3-minute onboarding path
8
+
9
+ 1. Install package.
10
+ 2. Paste the starter component.
11
+ 3. Confirm `onComplete` fires.
12
+
13
+ ```bash
14
+ npm install @secmia/openui-flow
15
+ ```
16
+
17
+ ```tsx
18
+ 'use client';
19
+
20
+ import {
21
+ AdaptiveFlow,
22
+ DefaultAppRequirements,
23
+ type AdaptiveFlowAdapter,
24
+ } from '@secmia/openui-flow';
25
+
26
+ type AppRequirement =
27
+ | (typeof DefaultAppRequirements)[keyof typeof DefaultAppRequirements]
28
+ | 'selected_plan';
29
+
30
+ const requirements: AppRequirement[] = [
31
+ DefaultAppRequirements.HAS_EMAIL,
32
+ DefaultAppRequirements.EMAIL_VERIFIED,
33
+ DefaultAppRequirements.HAS_PASSWORD,
34
+ DefaultAppRequirements.HAS_FIRST_NAME,
35
+ DefaultAppRequirements.ACCEPTED_TOS,
36
+ 'selected_plan',
37
+ ];
38
+
39
+ const adapter: AdaptiveFlowAdapter = {
40
+ async lookupByEmail(email) {
41
+ const exists = email.endsWith('@company.com');
42
+ return {
43
+ accountExists: exists,
44
+ hasPassword: exists,
45
+ isVerified: false,
46
+ agreedToTos: false,
47
+ profile: { firstName: null, lastName: null, jobTitle: null },
48
+ };
49
+ },
50
+ async requestOtp(email) {
51
+ console.log('request otp', email);
52
+ },
53
+ async verifyOtp(email, code) {
54
+ console.log('verify otp', email, code);
55
+ },
56
+ };
57
+
58
+ export default function FlowPage() {
59
+ return (
60
+ <AdaptiveFlow
61
+ requirements={requirements}
62
+ adapter={adapter}
63
+ persistence={{ key: 'flow-state:v1', storage: 'session' }}
64
+ onComplete={(context) => {
65
+ console.log('flow complete', context);
66
+ }}
67
+ />
68
+ );
69
+ }
70
+ ```
71
+
72
+ Expected result:
73
+ - User progresses through required steps.
74
+ - Flow ends at complete state.
75
+ - `onComplete` receives final context.
76
+
77
+ Peer dependencies:
78
+ - `react` `^18 || ^19`
79
+ - `react-dom` `^18 || ^19`
80
+
81
+ ---
82
+
83
+ ## Quick answers
84
+
85
+ Can users add their own flows?
86
+ - Yes. This is first-class.
87
+ - Define your own requirements, graph, step keys, resolver logic, and UI rendering.
88
+
89
+ When should I use requirements vs requirementGraph?
90
+ - Use `requirements` for the fastest start.
91
+ - Use `requirementGraph` when you need priorities, conditions, or dependencies.
92
+
93
+ How do I keep onboarding logic local to my app?
94
+ - Create an app-specific requirement union near your flow module.
95
+ - Include defaults and your custom requirements in one list.
96
+
97
+ ---
98
+
99
+ ## First customization: local requirement contract
100
+
101
+ ```ts
102
+ import { DefaultAppRequirements } from '@secmia/openui-flow';
103
+
104
+ type DefaultRequirement =
105
+ (typeof DefaultAppRequirements)[keyof typeof DefaultAppRequirements];
106
+
107
+ type ProductRequirement =
108
+ | DefaultRequirement
109
+ | 'selected_plan'
110
+ | 'connected_slack'
111
+ | 'has_workspace_name';
112
+
113
+ export const requirements: ProductRequirement[] = [
114
+ DefaultAppRequirements.HAS_EMAIL,
115
+ DefaultAppRequirements.EMAIL_VERIFIED,
116
+ DefaultAppRequirements.HAS_PASSWORD,
117
+ DefaultAppRequirements.HAS_FIRST_NAME,
118
+ DefaultAppRequirements.ACCEPTED_TOS,
119
+ 'selected_plan',
120
+ 'connected_slack',
121
+ ];
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Next level: graph mode
127
+
128
+ Use this when you need priority routing, conditional activation, dependency gates, or async resolvers.
129
+
130
+ ```tsx
131
+ import {
132
+ AdaptiveFlow,
133
+ DefaultAdaptiveSteps,
134
+ createRequirementGraph,
135
+ type AdaptiveContextBase,
136
+ type RequirementResolver,
137
+ } from '@secmia/openui-flow';
138
+
139
+ type AppContext = AdaptiveContextBase & {
140
+ riskLevel: 'low' | 'high';
141
+ useSso: boolean;
142
+ selectedPlan: 'starter' | 'pro' | null;
143
+ };
144
+
145
+ type Req =
146
+ | 'has_email'
147
+ | 'email_verified'
148
+ | 'has_password'
149
+ | 'accepted_tos'
150
+ | 'selected_plan';
151
+
152
+ type Step =
153
+ | 'COLLECT_EMAIL'
154
+ | 'VERIFY_OTP'
155
+ | 'COLLECT_PASSWORD'
156
+ | 'COLLECT_TOS'
157
+ | 'SELECT_PLAN'
158
+ | 'COMPLETE';
159
+
160
+ const requirements: Req[] = [
161
+ 'has_email',
162
+ 'email_verified',
163
+ 'has_password',
164
+ 'accepted_tos',
165
+ 'selected_plan',
166
+ ];
167
+
168
+ const resolvers: Partial<Record<Req, RequirementResolver<AppContext, Step>>> = {
169
+ has_email: { step: 'COLLECT_EMAIL', isMet: (ctx) => Boolean(ctx.email) },
170
+ email_verified: { step: 'VERIFY_OTP', isMet: async (ctx) => ctx.isVerified },
171
+ has_password: { step: 'COLLECT_PASSWORD', isMet: (ctx) => ctx.hasPassword },
172
+ accepted_tos: { step: 'COLLECT_TOS', isMet: (ctx) => ctx.agreedToTos },
173
+ selected_plan: { step: 'SELECT_PLAN', isMet: (ctx) => Boolean(ctx.selectedPlan) },
174
+ };
175
+
176
+ const graph = createRequirementGraph(requirements, resolvers, {
177
+ priorities: {
178
+ email_verified: 20,
179
+ has_password: 10,
180
+ selected_plan: 5,
181
+ },
182
+ conditions: {
183
+ has_password: (ctx) => !ctx.useSso,
184
+ email_verified: (ctx) => ctx.riskLevel === 'high',
185
+ },
186
+ dependencies: {
187
+ selected_plan: ['accepted_tos'],
188
+ },
189
+ });
190
+
191
+ <AdaptiveFlow<AppContext, Req, Step>
192
+ requirementGraph={graph}
193
+ completeStep={DefaultAdaptiveSteps.COMPLETE}
194
+ />;
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Advanced: maximum configuration
200
+
201
+ This demonstrates almost every major capability in one setup.
202
+
203
+ ```tsx
204
+ 'use client';
205
+
206
+ import {
207
+ AdaptiveFlow,
208
+ DefaultAdaptiveSteps,
209
+ createDefaultRequirementGraph,
210
+ type AdaptiveContextBase,
211
+ type AdaptiveFlowAdapter,
212
+ type AdaptiveFlowValidators,
213
+ type RequirementResolver,
214
+ } from '@secmia/openui-flow';
215
+
216
+ type ProductContext = AdaptiveContextBase & {
217
+ selectedPlan: 'starter' | 'pro' | null;
218
+ workspaceName: string | null;
219
+ riskScore: number;
220
+ useSso: boolean;
221
+ };
222
+
223
+ type ProductRequirement =
224
+ | 'has_email'
225
+ | 'email_verified'
226
+ | 'has_password'
227
+ | 'has_first_name'
228
+ | 'accepted_tos'
229
+ | 'selected_plan'
230
+ | 'has_workspace_name';
231
+
232
+ type ProductStep =
233
+ | 'COLLECT_EMAIL'
234
+ | 'VERIFY_OTP'
235
+ | 'COLLECT_PASSWORD'
236
+ | 'COLLECT_PROFILE'
237
+ | 'COLLECT_TOS'
238
+ | 'SELECT_PLAN'
239
+ | 'COLLECT_WORKSPACE'
240
+ | 'COMPLETE';
241
+
242
+ const resolvers: Partial<Record<ProductRequirement, RequirementResolver<ProductContext, ProductStep>>> = {
243
+ selected_plan: {
244
+ step: 'SELECT_PLAN',
245
+ isMet: (ctx) => Boolean(ctx.selectedPlan),
246
+ },
247
+ has_workspace_name: {
248
+ step: 'COLLECT_WORKSPACE',
249
+ isMet: (ctx) => Boolean(ctx.workspaceName),
250
+ },
251
+ };
252
+
253
+ const graph = createDefaultRequirementGraph<ProductContext, ProductRequirement, ProductStep>({
254
+ requirements: [
255
+ 'has_email',
256
+ 'email_verified',
257
+ 'has_password',
258
+ 'has_first_name',
259
+ 'accepted_tos',
260
+ 'selected_plan',
261
+ 'has_workspace_name',
262
+ ],
263
+ resolvers,
264
+ priorities: {
265
+ email_verified: 50,
266
+ has_password: 40,
267
+ selected_plan: 30,
268
+ },
269
+ conditions: {
270
+ has_password: (ctx) => !ctx.useSso,
271
+ email_verified: (ctx) => ctx.riskScore >= 70,
272
+ },
273
+ dependencies: {
274
+ has_workspace_name: ['selected_plan'],
275
+ },
276
+ });
277
+
278
+ const adapter: AdaptiveFlowAdapter<ProductContext> = {
279
+ async lookupByEmail(email) {
280
+ const response = await fetch('/api/identity/lookup', {
281
+ method: 'POST',
282
+ headers: { 'content-type': 'application/json' },
283
+ body: JSON.stringify({ email }),
284
+ });
285
+ if (!response.ok) {
286
+ throw new Error('Lookup failed.');
287
+ }
288
+ return response.json();
289
+ },
290
+ async requestOtp(email) {
291
+ await fetch('/api/identity/request-otp', {
292
+ method: 'POST',
293
+ headers: { 'content-type': 'application/json' },
294
+ body: JSON.stringify({ email }),
295
+ });
296
+ },
297
+ async verifyOtp(email, code) {
298
+ await fetch('/api/identity/verify-otp', {
299
+ method: 'POST',
300
+ headers: { 'content-type': 'application/json' },
301
+ body: JSON.stringify({ email, code }),
302
+ });
303
+ },
304
+ async saveProfile(context) {
305
+ await fetch('/api/profile', {
306
+ method: 'PATCH',
307
+ headers: { 'content-type': 'application/json' },
308
+ body: JSON.stringify(context.profile),
309
+ });
310
+ },
311
+ async acceptTos() {
312
+ await fetch('/api/legal/accept', { method: 'POST' });
313
+ },
314
+ async startOAuth(provider) {
315
+ window.location.href = `/api/identity/${provider}`;
316
+ },
317
+ async completeOAuth() {
318
+ return {
319
+ isVerified: true,
320
+ hasPassword: true,
321
+ };
322
+ },
323
+ };
324
+
325
+ const validators: AdaptiveFlowValidators<ProductContext> = {
326
+ email: (value) => {
327
+ if (!value.endsWith('@company.com')) {
328
+ return 'Company email is required.';
329
+ }
330
+ },
331
+ password: (value, { hasPassword }) => {
332
+ if (!hasPassword && value.length < 12) {
333
+ return 'Password must be at least 12 characters.';
334
+ }
335
+ },
336
+ };
337
+
338
+ export default function EnterpriseFlow() {
339
+ return (
340
+ <AdaptiveFlow<ProductContext, ProductRequirement, ProductStep>
341
+ requirementGraph={graph}
342
+ completeStep={DefaultAdaptiveSteps.COMPLETE}
343
+ adapter={adapter}
344
+ validators={validators}
345
+ persistence={{
346
+ key: 'enterprise-flow-state:v1',
347
+ storage: 'session',
348
+ clearOnComplete: true,
349
+ }}
350
+ stepTitles={{
351
+ SELECT_PLAN: 'Choose your plan',
352
+ COLLECT_WORKSPACE: 'Name your workspace',
353
+ }}
354
+ stepRegistry={{
355
+ SELECT_PLAN: ({ setContextPatch }) => (
356
+ <button onClick={() => setContextPatch({ selectedPlan: 'pro' })}>Select Pro Plan</button>
357
+ ),
358
+ COLLECT_WORKSPACE: ({ setContextPatch }) => (
359
+ <button onClick={() => setContextPatch({ workspaceName: 'Acme HQ' })}>Set Workspace</button>
360
+ ),
361
+ }}
362
+ renderStep={({ defaultView, transitions }) => (
363
+ <div>
364
+ <p>Transitions so far: {transitions.length}</p>
365
+ {defaultView}
366
+ </div>
367
+ )}
368
+ onStepTransition={(transition) => {
369
+ console.log('transition', transition);
370
+ }}
371
+ onError={(error) => {
372
+ console.error('flow error', error);
373
+ }}
374
+ onComplete={(context) => {
375
+ console.log('complete context', context);
376
+ }}
377
+ classNames={{
378
+ shell: 'rounded-xl border p-6 bg-white',
379
+ title: 'text-xl font-semibold',
380
+ }}
381
+ styles={{
382
+ shell: { maxWidth: 720 },
383
+ }}
384
+ unstyled={false}
385
+ initialValue={{
386
+ selectedPlan: null,
387
+ workspaceName: null,
388
+ riskScore: 80,
389
+ useSso: false,
390
+ }}
391
+ />
392
+ );
393
+ }
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Custom UI: full control options
399
+
400
+ You have 4 UI customization levels, from quick theming to complete headless control.
401
+
402
+ ### Level 1: style and class overrides
403
+
404
+ Keep built-in UI structure, but override styles/classes for all slots.
405
+
406
+ ```tsx
407
+ <AdaptiveFlow
408
+ classNames={{
409
+ shell: 'rounded-2xl border border-zinc-200 p-8',
410
+ title: 'text-2xl font-bold tracking-tight',
411
+ oauthButton: 'h-11 rounded-lg border px-4',
412
+ error: 'text-sm text-red-700 bg-red-50 rounded p-3',
413
+ }}
414
+ styles={{
415
+ shell: { maxWidth: 680, margin: '0 auto' },
416
+ title: { letterSpacing: '-0.01em' },
417
+ }}
418
+ />
419
+ ```
420
+
421
+ Use `unstyled` to disable built-in inline defaults.
422
+
423
+ ```tsx
424
+ <AdaptiveFlow unstyled classNames={{ shell: 'my-own-layout-root' }} />
425
+ ```
426
+
427
+ ### Level 2: custom UI for specific steps
428
+
429
+ Use `stepRegistry` to replace selected steps with your own components while leaving other steps on defaults.
430
+
431
+ ```tsx
432
+ <AdaptiveFlow
433
+ stepRegistry={{
434
+ SELECT_PLAN: ({ setContextPatch, run }) => (
435
+ <div>
436
+ <h3>Choose a plan</h3>
437
+ <button
438
+ onClick={() => {
439
+ void run(async () => {
440
+ setContextPatch({ selectedPlan: 'starter' });
441
+ });
442
+ }}
443
+ >
444
+ Starter
445
+ </button>
446
+ <button
447
+ onClick={() => {
448
+ void run(async () => {
449
+ setContextPatch({ selectedPlan: 'pro' });
450
+ });
451
+ }}
452
+ >
453
+ Pro
454
+ </button>
455
+ </div>
456
+ ),
457
+ }}
458
+ />
459
+ ```
460
+
461
+ ### Level 3: custom renderer for all steps
462
+
463
+ Use `renderStep` for full step-body ownership. You can still reuse `defaultView` when useful.
464
+
465
+ ```tsx
466
+ <AdaptiveFlow
467
+ renderStep={({ step, defaultView, transitions, context, setContextPatch }) => (
468
+ <section>
469
+ <header>
470
+ <h2>Current step: {step}</h2>
471
+ <p>Transitions: {transitions.length}</p>
472
+ </header>
473
+
474
+ <aside>
475
+ <button onClick={() => setContextPatch({ locale: 'en-US' } as Partial<typeof context>)}>
476
+ Set locale
477
+ </button>
478
+ </aside>
479
+
480
+ <main>{defaultView}</main>
481
+ </section>
482
+ )}
483
+ />
484
+ ```
485
+
486
+ ### Level 4: fully custom UI for everything (headless mode)
487
+
488
+ If you want total control over shell, header, footer, button layout, and every interaction, use engine APIs directly.
489
+
490
+ ```tsx
491
+ 'use client';
492
+
493
+ import { useEffect, useMemo, useState } from 'react';
494
+ import {
495
+ createRequirementGraph,
496
+ evaluateNextStep,
497
+ getMissingRequirements,
498
+ DefaultAdaptiveSteps,
499
+ type AdaptiveContextBase,
500
+ type RequirementResolver,
501
+ } from '@secmia/openui-flow';
502
+
503
+ type Ctx = AdaptiveContextBase & {
504
+ selectedPlan: 'starter' | 'pro' | null;
505
+ };
506
+
507
+ type Req = 'has_email' | 'email_verified' | 'selected_plan';
508
+ type Step = 'COLLECT_EMAIL' | 'VERIFY_OTP' | 'SELECT_PLAN' | 'COMPLETE';
509
+
510
+ const resolvers: Partial<Record<Req, RequirementResolver<Ctx, Step>>> = {
511
+ has_email: { step: 'COLLECT_EMAIL', isMet: (ctx) => Boolean(ctx.email) },
512
+ email_verified: { step: 'VERIFY_OTP', isMet: (ctx) => ctx.isVerified },
513
+ selected_plan: { step: 'SELECT_PLAN', isMet: (ctx) => Boolean(ctx.selectedPlan) },
514
+ };
515
+
516
+ export default function FullyCustomAuth() {
517
+ const [context, setContext] = useState<Ctx>({
518
+ email: null,
519
+ isVerified: false,
520
+ hasPassword: false,
521
+ agreedToTos: false,
522
+ profile: null,
523
+ selectedPlan: null,
524
+ });
525
+ const [step, setStep] = useState<Step>('COLLECT_EMAIL');
526
+ const [missing, setMissing] = useState<Req[]>([]);
527
+
528
+ const graph = useMemo(
529
+ () => createRequirementGraph<Ctx, Req, Step>(['has_email', 'email_verified', 'selected_plan'], resolvers),
530
+ [],
531
+ );
532
+
533
+ useEffect(() => {
534
+ void (async () => {
535
+ const [nextStep, missingReqs] = await Promise.all([
536
+ evaluateNextStep(context, graph, DefaultAdaptiveSteps.COMPLETE as Step),
537
+ getMissingRequirements(context, graph),
538
+ ]);
539
+ setStep(nextStep);
540
+ setMissing(missingReqs);
541
+ })();
542
+ }, [context, graph]);
543
+
544
+ return (
545
+ <div>
546
+ <header>
547
+ <h1>My Brand Auth</h1>
548
+ <p>
549
+ Progress: {3 - missing.length}/3
550
+ </p>
551
+ </header>
552
+
553
+ {step === 'COLLECT_EMAIL' ? (
554
+ <button onClick={() => setContext((prev) => ({ ...prev, email: 'user@company.com' }))}>
555
+ Mock email submit
556
+ </button>
557
+ ) : null}
558
+
559
+ {step === 'VERIFY_OTP' ? (
560
+ <button onClick={() => setContext((prev) => ({ ...prev, isVerified: true }))}>
561
+ Mock verify
562
+ </button>
563
+ ) : null}
564
+
565
+ {step === 'SELECT_PLAN' ? (
566
+ <button onClick={() => setContext((prev) => ({ ...prev, selectedPlan: 'pro' }))}>
567
+ Choose Pro
568
+ </button>
569
+ ) : null}
570
+
571
+ {step === 'COMPLETE' ? <p>All done.</p> : null}
572
+ </div>
573
+ );
574
+ }
575
+ ```
576
+
577
+ Precedence rule inside `AdaptiveFlow`:
578
+ - `renderStep` wins first.
579
+ - If `renderStep` is not provided, `stepRegistry[step]` is used.
580
+ - If neither is provided, built-in default step UI is rendered.
581
+
582
+ This gives you a clean path from quick defaults to complete custom UI ownership.
583
+
584
+ ---
585
+
586
+ ## Integration recipes
587
+
588
+ ### Next.js App Router
589
+
590
+ ```tsx
591
+ 'use client';
592
+
593
+ import { useRouter } from 'next/navigation';
594
+ import {
595
+ AdaptiveFlow,
596
+ DefaultAppRequirements,
597
+ type AdaptiveFlowAdapter,
598
+ } from '@secmia/openui-flow';
599
+
600
+ type AppRequirement =
601
+ | (typeof DefaultAppRequirements)[keyof typeof DefaultAppRequirements]
602
+ | 'has_workspace_name';
603
+
604
+ const requirements: AppRequirement[] = [
605
+ DefaultAppRequirements.HAS_EMAIL,
606
+ DefaultAppRequirements.EMAIL_VERIFIED,
607
+ DefaultAppRequirements.HAS_PASSWORD,
608
+ DefaultAppRequirements.HAS_FIRST_NAME,
609
+ DefaultAppRequirements.ACCEPTED_TOS,
610
+ ];
611
+
612
+ const adapter: AdaptiveFlowAdapter = {
613
+ async lookupByEmail(email) {
614
+ const response = await fetch('/api/identity/lookup', {
615
+ method: 'POST',
616
+ headers: { 'content-type': 'application/json' },
617
+ body: JSON.stringify({ email }),
618
+ });
619
+ if (!response.ok) {
620
+ throw new Error('Lookup failed.');
621
+ }
622
+ return response.json();
623
+ },
624
+ };
625
+
626
+ export default function SignInClient() {
627
+ const router = useRouter();
628
+
629
+ return (
630
+ <AdaptiveFlow
631
+ adapter={adapter}
632
+ requirements={requirements}
633
+ persistence={{ key: 'next-flow-state:v1', storage: 'session' }}
634
+ onComplete={() => router.replace('/dashboard')}
635
+ />
636
+ );
637
+ }
638
+ ```
639
+
640
+ ### Supabase
641
+
642
+ ```tsx
643
+ 'use client';
644
+
645
+ import { createClient } from '@supabase/supabase-js';
646
+ import { AdaptiveFlow, type AdaptiveFlowAdapter } from '@secmia/openui-flow';
647
+
648
+ const supabase = createClient(
649
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
650
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
651
+ );
652
+
653
+ const adapter: AdaptiveFlowAdapter = {
654
+ async requestOtp(email) {
655
+ const { error } = await supabase.auth.signInWithOtp({ email });
656
+ if (error) {
657
+ throw new Error(error.message);
658
+ }
659
+ },
660
+ async verifyOtp(email, code) {
661
+ const { error } = await supabase.auth.verifyOtp({ email, token: code, type: 'email' });
662
+ if (error) {
663
+ throw new Error(error.message);
664
+ }
665
+ },
666
+ async startOAuth(provider) {
667
+ const { error } = await supabase.auth.signInWithOAuth({
668
+ provider,
669
+ options: { redirectTo: `${window.location.origin}/auth/callback` },
670
+ });
671
+ if (error) {
672
+ throw new Error(error.message);
673
+ }
674
+ },
675
+ };
676
+
677
+ export default function SupabaseAuth() {
678
+ return <AdaptiveFlow adapter={adapter} persistence={{ key: 'supabase-auth:v1' }} />;
679
+ }
680
+ ```
681
+
682
+ ### Firebase
683
+
684
+ ```tsx
685
+ 'use client';
686
+
687
+ import { getAuth, GoogleAuthProvider, OAuthProvider, signInWithPopup } from 'firebase/auth';
688
+ import { AdaptiveFlow, type AdaptiveFlowAdapter } from '@secmia/openui-flow';
689
+ import { app } from './firebase';
690
+
691
+ const auth = getAuth(app);
692
+
693
+ const adapter: AdaptiveFlowAdapter = {
694
+ async startOAuth(provider) {
695
+ if (provider === 'google') {
696
+ await signInWithPopup(auth, new GoogleAuthProvider());
697
+ return;
698
+ }
699
+ await signInWithPopup(auth, new OAuthProvider('apple.com'));
700
+ },
701
+ };
702
+
703
+ export default function FirebaseAuth() {
704
+ return <AdaptiveFlow adapter={adapter} persistence={{ key: 'firebase-auth:v1' }} />;
705
+ }
706
+ ```
707
+
708
+ ---
709
+
710
+ ## Engine-only usage
711
+
712
+ ```ts
713
+ import {
714
+ createRequirementGraph,
715
+ evaluateNextStep,
716
+ getMissingRequirements,
717
+ DefaultAdaptiveSteps,
718
+ } from '@secmia/openui-flow';
719
+
720
+ const nextStep = await evaluateNextStep(context, graph, DefaultAdaptiveSteps.COMPLETE);
721
+ const missing = await getMissingRequirements(context, graph);
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Core concepts
727
+
728
+ - Context: current auth and profile state.
729
+ - Requirement: business condition that must be satisfied.
730
+ - Resolver: checker function for one requirement.
731
+ - Step: current UI state for unmet requirement.
732
+ - RequirementGraph: graph nodes with optional priority, condition, and dependency metadata.
733
+
734
+ Evaluation loop:
735
+ 1. Build graph.
736
+ 2. Evaluate active nodes.
737
+ 3. Select highest-priority unmet node.
738
+ 4. Render mapped step.
739
+ 5. Update context.
740
+ 6. Repeat until complete step.
741
+
742
+ ---
743
+
744
+ ## API reference
745
+
746
+ Primary exports:
747
+ - `AdaptiveFlow`
748
+ - `defaultRequirements`
749
+ - `initialContext`
750
+ - `defaultRequirementResolvers`
751
+ - `createDefaultRequirementGraph`
752
+ - `createRequirementGraph`
753
+ - `evaluateNextStep`
754
+ - `getMissingRequirements`
755
+ - `DefaultAdaptiveSteps`
756
+ - `DefaultAppRequirements`
757
+
758
+ Important types:
759
+ - `AdaptiveFlowProps`
760
+ - `AdaptiveFlowAdapter`
761
+ - `AdaptiveFlowValidators`
762
+ - `AdaptiveFlowPersistence`
763
+ - `AdaptiveStepRegistry`
764
+ - `AdaptiveStepRendererArgs`
765
+ - `AdaptiveStepRenderArgs`
766
+ - `AdaptiveStepTransition`
767
+ - `RequirementResolver`
768
+ - `RequirementGraph`
769
+ - `RequirementGraphNode`
770
+ - `AdaptiveContext`
771
+ - `AdaptiveContextBase`
772
+
773
+ ---
774
+
775
+ ## Release checklist
776
+
777
+ - Ensure adapter methods return clear, user-safe errors.
778
+ - Version persistence keys (example: `flow-state:v1`).
779
+ - Track analytics with `onStepTransition`.
780
+ - Keep requirements and graph definitions local to each product module.
781
+ - Add integration tests for all flow branches.
782
+ - Run `npm run typecheck` and `npm run build` before every publish.
783
+
784
+ ---
785
+
786
+ ## Troubleshooting
787
+
788
+ Step not appearing:
789
+ - Cause: priority, condition, or dependency suppresses node activation.
790
+ - Fix: inspect graph `priorities`, `conditions`, and `dependencies`.
791
+
792
+ State resets on refresh:
793
+ - Cause: persistence missing or wrong key.
794
+ - Fix: set `persistence.key` and chosen storage.
795
+
796
+ OAuth return does not resume:
797
+ - Cause: missing `completeOAuth` or persistence disabled.
798
+ - Fix: implement `completeOAuth` and enable persistence.
799
+
800
+ ---
801
+
802
+ ## License
803
+
804
+ MIT