@secmia/openui-flow
v4.2.8
Published
Backend-agnostic adaptive flow engine for React apps
Maintainers
Readme
@secmia/openui-flow
Adaptive, graph-driven multi-step flow orchestration for React.
Build authentication, onboarding, compliance, setup, and gating flows with one engine:
- requirement-based routing
- dependency-aware graph evaluation
- headless hook + default UI
- adapter-driven backend integration
- persistence, validation, retry/backoff
For AI agents
Use AGENTS.md for implementation debrief, architecture notes, and workflow guidance.
Install
npm install @secmia/openui-flowPeer dependencies:
react^18 || ^19react-dom^18 || ^19
3-minute quickstart (Next.js App Router safe)
Important: in Next.js App Router, functions cannot be passed from Server Components to Client Components. Keep
AdaptiveFlow, adapter functions, and handlers inside a Client Component.
// app/flow-demo.tsx
'use client';
import {
AdaptiveFlow,
DefaultAppRequirements,
type AdaptiveFlowAdapter,
} from '@secmia/openui-flow';
type AppRequirement =
| (typeof DefaultAppRequirements)[keyof typeof DefaultAppRequirements]
| 'selected_plan';
const requirements: AppRequirement[] = [
DefaultAppRequirements.HAS_EMAIL,
DefaultAppRequirements.EMAIL_VERIFIED,
DefaultAppRequirements.HAS_PASSWORD,
DefaultAppRequirements.HAS_FIRST_NAME,
DefaultAppRequirements.HAS_LAST_NAME,
DefaultAppRequirements.HAS_JOB_TITLE,
DefaultAppRequirements.ACCEPTED_TOS,
'selected_plan',
];
const adapter: AdaptiveFlowAdapter = {
async lookupByEmail(email) {
const exists = email.endsWith('@company.com');
return {
accountExists: exists,
hasPassword: exists,
isVerified: false,
agreedToTos: false,
profile: { firstName: null, lastName: null, jobTitle: null },
};
},
async requestOtp(email) {
console.log('request otp', email);
},
async verifyOtp(email, code) {
console.log('verify otp', email, code);
},
};
export default function FlowDemo() {
return (
<AdaptiveFlow
requirements={requirements}
adapter={adapter}
persistence={{ key: 'flow-state:v1', storage: 'session' }}
onComplete={(context) => {
console.log('flow complete', context);
}}
/>
);
}// app/page.tsx (Server Component)
import FlowDemo from './flow-demo';
export default function Page() {
return <FlowDemo />;
}What this package is
@secmia/openui-flow is a generic flow engine with a React-first API.
You define:
- requirements (conditions that must be met)
- resolvers (how each requirement is evaluated)
- graph metadata (priority, dependency, conditional activation)
The engine decides the next step; UI can be default, partially custom, or fully headless.
Core model
Context: current user/product/session state.Requirement: business condition (has_email,accepted_tos, etc.).Resolver: maps requirement toisMet(context)+ fallback step.RequirementGraph: ordered nodes withpriority,dependsOn,when.
Graph guarantees:
- cycle detection at graph creation
- dependency validation
- dependency-aware topological ordering
- deterministic tie-breaking
API overview
Primary exports:
AdaptiveFlowuseAdaptiveFlowcreateRequirementGraphcreateDefaultRequirementGraphevaluateNextStepgetMissingRequirementsdefaultRequirementsdefaultRequirementResolversinitialContextDefaultAppRequirementsDefaultAdaptiveSteps
High-value types:
AdaptiveFlowPropsAdaptiveFlowAdapterUseAdaptiveFlowOptionsUseAdaptiveFlowResultAdaptiveFlowValidatorsAdaptiveFlowSchemasAdaptiveFlowPersistenceAdaptiveFlowRetryPolicyAdaptiveFlowOAuthProviderRequirementGraphRequirementResolver
UI customization levels
Level 1: style/class overrides
<AdaptiveFlow
classNames={{
shell: 'rounded-xl border p-6',
title: 'text-xl font-semibold',
oauthButton: 'h-10 rounded border px-3',
}}
styles={{
shell: { maxWidth: 720 },
}}
/>Use unstyled to disable default inline styles.
Level 2: per-step overrides (stepRegistry)
<AdaptiveFlow
stepRegistry={{
SELECT_PLAN: ({ setContextPatch }) => (
<button onClick={() => setContextPatch({ selectedPlan: 'pro' })}>Select Pro</button>
),
}}
/>Level 3: full step renderer (renderStep)
<AdaptiveFlow
renderStep={({ step, defaultView, transitions }) => (
<section>
<h2>{step}</h2>
<p>Transitions: {transitions.length}</p>
{defaultView}
</section>
)}
/>Level 4: fully headless (useAdaptiveFlow)
'use client';
import { useAdaptiveFlow } from '@secmia/openui-flow';
export default function HeadlessFlow() {
const {
step,
busy,
fieldErrors,
handleEmail,
handleOtp,
setContextPatch,
} = useAdaptiveFlow({
requirements: ['has_email', 'email_verified', 'accepted_tos'],
completeStep: 'COMPLETE',
});
return (
<div>
<p>Step: {step}</p>
{fieldErrors.email ? <p>{fieldErrors.email}</p> : null}
<button disabled={busy} onClick={() => handleEmail('[email protected]')}>Submit Email</button>
<button disabled={busy} onClick={() => handleOtp('123456')}>Submit OTP</button>
<button onClick={() => setContextPatch({ agreedToTos: true })}>Accept TOS</button>
</div>
);
}Validation and schemas
Validators can return:
string{ field, message }void- async variants of the same
const validators = {
password: (value: string) => {
if (value.length < 12) {
return { field: 'password', message: 'Password must be at least 12 characters.' };
}
},
};Schema adapters are Zod-friendly (safeParse or parse):
import { z } from 'zod';
<AdaptiveFlow
schemas={{
email: z.string().email(),
}}
/>Persistence
<AdaptiveFlow
persistence={{
key: 'flow-state:v1',
storage: 'local',
clearOnComplete: true,
onError: (error, phase) => {
console.error('persistence', phase, error);
},
}}
/>Retry/backoff (adapter calls)
<AdaptiveFlow
retryPolicy={{
maxAttempts: 4,
initialDelayMs: 200,
factor: 2,
maxDelayMs: 2000,
jitter: true,
shouldRetry: (error) => error.name !== 'FlowValidationError',
}}
/>Notes:
- retries apply to adapter calls (
lookupByEmail,requestOtp,verifyOtp, etc.) - validation errors are not retried unless your
shouldRetryallows them
OAuth
- Default footer shows Google and Apple.
- Override provider buttons with
oauthProviders. - Redirect URL is owned by your adapter�s
startOAuthimplementation.
<AdaptiveFlow
oauthProviders={[
{ id: 'google', label: 'Continue with Google' },
{ id: 'github', label: 'Continue with GitHub' },
]}
/>const adapter = {
async startOAuth(provider: string) {
window.location.href = `/api/identity/${provider}?redirectTo=${encodeURIComponent(`${window.location.origin}/auth/callback`)}`;
},
async completeOAuth() {
return { isVerified: true };
},
};SSR and hydration
- Storage reads/writes run only in the browser.
- On SSR, flow starts with defaults/
initialValueand hydrates client state on mount. - For minimal UI flash, seed
initialValuefrom server-known session hints.
Troubleshooting
- Next.js "Functions cannot be passed to Client Components": keep adapter and handlers in a Client Component.
- Step not appearing: verify
priority,dependsOn, andwhen. - State reset: check
persistence.keyand storage mode. - OAuth not resuming: implement
completeOAuthand enable persistence.
Release checklist
npm run typechecknpm run build- Verify adapter errors are user-safe and actionable
- Use versioned persistence keys (for example
flow-state:v1)
License
MIT
