npm package: @secmia/openui-flow
Description: Backend-agnostic adaptive flow engine for React apps
Latest version: 4.2.8

# @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](AGENTS.md) for implementation debrief, architecture notes, and workflow guidance.

## Install

```bash
npm install @secmia/openui-flow
```

Peer dependencies:
- `react` `^18 || ^19`
- `react-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.

```tsx
// 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);
      }}
    />
  );
}
```

```tsx
// 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 to `isMet(context)` + fallback step.
- `RequirementGraph`: ordered nodes with `priority`, `dependsOn`, `when`.

Graph guarantees:
- cycle detection at graph creation
- dependency validation
- dependency-aware topological ordering
- deterministic tie-breaking

## API overview

Primary exports:
- `AdaptiveFlow`
- `useAdaptiveFlow`
- `createRequirementGraph`
- `createDefaultRequirementGraph`
- `evaluateNextStep`
- `getMissingRequirements`
- `defaultRequirements`
- `defaultRequirementResolvers`
- `initialContext`
- `DefaultAppRequirements`
- `DefaultAdaptiveSteps`

High-value types:
- `AdaptiveFlowProps`
- `AdaptiveFlowAdapter`
- `UseAdaptiveFlowOptions`
- `UseAdaptiveFlowResult`
- `AdaptiveFlowValidators`
- `AdaptiveFlowSchemas`
- `AdaptiveFlowPersistence`
- `AdaptiveFlowRetryPolicy`
- `AdaptiveFlowOAuthProvider`
- `RequirementGraph`
- `RequirementResolver`

## UI customization levels

### Level 1: style/class overrides

```tsx
<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`)

```tsx
<AdaptiveFlow
  stepRegistry={{
    SELECT_PLAN: ({ setContextPatch }) => (
      <button onClick={() => setContextPatch({ selectedPlan: 'pro' })}>Select Pro</button>
    ),
  }}
/>
```

### Level 3: full step renderer (`renderStep`)

```tsx
<AdaptiveFlow
  renderStep={({ step, defaultView, transitions }) => (
    <section>
      <h2>{step}</h2>
      <p>Transitions: {transitions.length}</p>
      {defaultView}
    </section>
  )}
/>
```

### Level 4: fully headless (`useAdaptiveFlow`)

```tsx
'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('user@company.com')}>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

```ts
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`):

```tsx
import { z } from 'zod';

<AdaptiveFlow
  schemas={{
    email: z.string().email(),
  }}
/>
```

## Persistence

```tsx
<AdaptiveFlow
  persistence={{
    key: 'flow-state:v1',
    storage: 'local',
    clearOnComplete: true,
    onError: (error, phase) => {
      console.error('persistence', phase, error);
    },
  }}
/>
```

## Retry/backoff (adapter calls)

```tsx
<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 `shouldRetry` allows them

## OAuth

- Default footer shows Google and Apple.
- Override provider buttons with `oauthProviders`.
- Redirect URL is owned by your adapter�s `startOAuth` implementation.

```tsx
<AdaptiveFlow
  oauthProviders={[
    { id: 'google', label: 'Continue with Google' },
    { id: 'github', label: 'Continue with GitHub' },
  ]}
/>
```

```ts
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/`initialValue` and hydrates client state on mount.
- For minimal UI flash, seed `initialValue` from 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`, and `when`.
- State reset: check `persistence.key` and storage mode.
- OAuth not resuming: implement `completeOAuth` and enable persistence.

## Release checklist

- `npm run typecheck`
- `npm run build`
- Verify adapter errors are user-safe and actionable
- Use versioned persistence keys (for example `flow-state:v1`)

## License

MIT