@owlmeans/client-flow
v0.1.1
Published
Client-side configurable user flow management library for OwlMeans Common applications. This package provides a comprehensive system for implementing complex user workflows, authentication flows, and multi-step processes in React applications with state p
Readme
@owlmeans/client-flow
Client-side configurable user flow management library for OwlMeans Common applications. This package provides a comprehensive system for implementing complex user workflows, authentication flows, and multi-step processes in React applications with state persistence and navigation integration.
Overview
The @owlmeans/client-flow package extends the base @owlmeans/flow package with client-specific functionality. It provides:
- Client Flow Management: Complete client-side implementation of configurable user flows
- State Persistence: Automatic flow state persistence across browser sessions
- Navigation Integration: Seamless integration with React Router navigation
- Authentication Flows: Pre-built authentication and authorization workflows
- Service Integration: Integration with OwlMeans service and module systems
- React Components: Flow-aware components and hooks for React applications
- Configuration Management: Dynamic flow configuration from external sources
- Error Handling: Robust error handling and recovery mechanisms
This package is part of the OwlMeans flow management quadra:
- @owlmeans/flow: Common flow interfaces and utilities
- @owlmeans/client-flow: Client-side flow implementation (this package)
- @owlmeans/web-flow: Web-specific flow components and utilities
- @owlmeans/server-flow: Server-side flow processing
Installation
npm install @owlmeans/client-flow reactCore Concepts
Flow Models
Flows are state machines that guide users through multi-step processes. Each flow consists of steps, transitions, and conditions that determine the user's path.
Flow States
Flow states represent the current position in a workflow and are automatically persisted to survive browser sessions and page reloads.
Flow Service
The central service that manages flow lifecycle, state transitions, and integration with other application services.
Flow Client
A client-side wrapper that provides an easy-to-use interface for flow operations with navigation integration.
API Reference
Factory Functions
makeBasicFlowService(alias?: string): FlowService
Creates a flow service instance for managing client-side flows.
import { makeBasicFlowService } from '@owlmeans/client-flow'
const flowService = makeBasicFlowService('user-flows')Parameters:
alias: string (optional) - Service alias for registration, defaults to 'flow'
Returns: FlowService instance ready for registration with context
createFlowClient<C, T>(context: T, nav: Navigator): FlowClient
Creates a flow client that integrates flows with navigation and context.
import { createFlowClient } from '@owlmeans/client-flow'
import { useContext, useNavigate } from '@owlmeans/client'
const context = useContext()
const navigator = useNavigate()
const flowClient = createFlowClient(context, navigator)Parameters:
context: T - Application context with flow servicenav: Navigator - Navigation service for routing integration
Returns: FlowClient instance for flow operations
Core Interfaces
FlowService
Main service interface for flow management and lifecycle operations.
interface FlowService extends LazyService {
supplied: Promise<boolean> // Flow availability promise
flow: FlowModel | null // Current active flow
// Flow operations
state(): Promise<FlowModel | null> // Get current flow state
begin(slug?: string, from?: string): Promise<FlowModel> // Start new flow
load(flow: string): Promise<FlowModel> // Load existing flow
proceed(req?, dryRun?): Promise<string> // Proceed to next step
// Configuration and providers
config(): FlowConfig // Get flow configuration
provideFlow: FlowProvider // Flow provider function
resolvePair(): ResolvePair // Promise resolution pair
}FlowClient
Client-side flow wrapper with navigation and persistence integration.
interface FlowClient {
boot(target?: string, from?: string): Promise<FlowClient> // Initialize flow client
setup(flow: FlowModel): FlowClient // Setup with existing flow
flow(): FlowModel // Get current flow model
service(): ResolvedServiceRoute // Get target service route
proceed(transition: FlowTransition, req?): Promise<void> // Execute transition
persist(): Promise<boolean> // Persist current state
}StateRecord
Interface for flow state persistence records.
interface StateRecord extends ResourceRecord, FlowState {
id: string // Record identifier
flow: string // Flow type identifier
step: string // Current step identifier
service: string // Target service
data: any // Flow-specific data
// ... other FlowState properties
}StateResource
Resource interface for managing flow state persistence.
interface StateResource extends ClientResource<StateRecord> {
// Inherits all standard resource methods for CRUD operations
}Flow Service Methods Detailed Reference
state(): Promise<FlowModel | null>
Purpose: Retrieves the current active flow model
Behavior:
- Waits for flow service to be supplied/ready
- Returns the currently active flow or null if no flow is active
- Used to check flow status before operations
Usage: Checking if a flow is currently active
const flowService = context.service<FlowService>('flow')
const currentFlow = await flowService.state()
if (currentFlow) {
console.log('Active flow:', currentFlow.name)
console.log('Current step:', currentFlow.step())
} else {
console.log('No active flow')
}begin(slug?: string, from?: string): Promise<FlowModel>
Purpose: Starts a new flow with the specified configuration
Behavior:
- Creates a new flow model based on the slug
- Sets the service as supplied/ready
- Enters the flow from the specified step or initial step
- Returns the active flow model
Usage: Starting authentication, registration, or other workflows
Parameters:
slug: string (optional) - Flow identifier, defaults to config.defaultFlow or STD_AUTH_FLOWfrom: string (optional) - Starting step, defaults to flow's initial step
const flowService = context.service<FlowService>('flow')
// Start default authentication flow
const authFlow = await flowService.begin()
// Start specific flow from specific step
const registrationFlow = await flowService.begin('user-registration', 'email-verification')
// Start flow with default slug
const defaultFlow = await flowService.begin('custom-onboarding')load(flow: string): Promise<FlowModel>
Purpose: Loads and activates a flow from serialized state
Behavior:
- Recreates flow model from serialized state string
- Sets the service as supplied/ready
- Restores flow to its previous state
- Returns the restored flow model
Usage: Restoring flows from persistent storage
const flowService = context.service<FlowService>('flow')
// Load flow from saved state
const savedFlowState = localStorage.getItem('flow-state')
if (savedFlowState) {
const restoredFlow = await flowService.load(savedFlowState)
console.log('Flow restored to step:', restoredFlow.step())
}proceed(req?: Partial<AbstractRequest>, dryRun?: boolean): Promise<string>
Purpose: Advances the flow to the next step based on conditions
Behavior:
- Evaluates current step's transitions and conditions
- Moves to the next appropriate step
- Throws FlowUnsupported error (not implemented in basic service)
- Should be overridden in specific implementations
Usage: Advancing through flow steps
Note: The basic service throws FlowUnsupported - this should be implemented by specific flow implementations
// This would be implemented in a specific flow service
const nextStep = await flowService.proceed({
body: { userInput: 'some-data' }
})config(): FlowConfig
Purpose: Retrieves the current flow configuration
Behavior:
- Accesses flow configuration from context
- Returns default empty config if not configured
- Used for flow behavior customization
Usage: Accessing flow settings and configuration
const flowService = context.service<FlowService>('flow')
const config = flowService.config()
console.log('Default flow:', config.defaultFlow)
console.log('Available services:', config.services)Flow Client Methods Detailed Reference
boot(target?: string, from?: string): Promise<FlowClient>
Purpose: Initializes the flow client with optional target service and starting point
Behavior:
- Waits for flow service to be ready
- Attempts to restore flow from persistent storage
- Creates new flow if no saved state exists
- Sets target service if provided
- Returns the initialized client
Usage: Starting the flow system in your application
const flowClient = createFlowClient(context, navigator)
// Boot with specific target service
await flowClient.boot('user-dashboard', 'login-step')
// Boot with default behavior
await flowClient.boot()
// Boot without target, from saved state
await flowClient.boot(null)setup(flow: FlowModel): FlowClient
Purpose: Configures the client with an existing flow model
Behavior:
- Sets the provided flow as the active flow
- Returns the client for method chaining
- Used when you already have a flow model
Usage: Using pre-configured flow models
const existingFlow = await flowService.begin('checkout-flow')
const flowClient = createFlowClient(context, navigator)
.setup(existingFlow)flow(): FlowModel
Purpose: Returns the current active flow model
Behavior: Direct access to the underlying flow model
Usage: Accessing flow state and methods
const currentFlow = flowClient.flow()
console.log('Current step:', currentFlow.step())
console.log('Flow data:', currentFlow.data())service(): ResolvedServiceRoute
Purpose: Returns the resolved service route for the target service
Behavior:
- Resolves the target service from the context
- Returns route information for navigation
Usage: Getting service routing information
const serviceRoute = flowClient.service()
console.log('Target service:', serviceRoute.service)
console.log('Service path:', serviceRoute.path)proceed(transition: FlowTransition, req?: Partial<AbstractRequest>): Promise<void>
Purpose: Executes a flow transition with optional request data
Behavior:
- Executes the specified transition
- Passes request data to the transition handler
- Updates flow state based on transition result
- Handles navigation if required
Usage: Moving between flow steps
await flowClient.proceed('next', {
body: { formData: userData }
})
await flowClient.proceed('back')
await flowClient.proceed('submit', {
params: { id: userId }
})persist(): Promise<boolean>
Purpose: Saves the current flow state to persistent storage
Behavior:
- Serializes current flow state
- Saves to flow state resource
- Returns success status
Usage: Saving flow progress
const saved = await flowClient.persist()
if (saved) {
console.log('Flow state saved successfully')
} else {
console.log('Failed to save flow state')
}Constants
DEFAULT_ALIAS
Default flow service alias ('flow').
FLOW_STATE
Resource identifier for flow state persistence ('state:flow').
REHACK_MOD
Internal module identifier for redirects ('__redirect').
Usage Examples
Basic Flow Setup
import { makeBasicFlowService, createFlowClient } from '@owlmeans/client-flow'
import { makeClientContext, useContext } from '@owlmeans/client'
// Create context with flow service
const context = makeClientContext(config)
const flowService = makeBasicFlowService()
context.registerService(flowService)
// Initialize context
await context.configure().init()
// Create flow client
function MyFlowComponent() {
const context = useContext()
const navigator = useNavigate()
const [flowClient, setFlowClient] = useState(null)
useEffect(() => {
const initFlow = async () => {
const client = createFlowClient(context, navigator)
await client.boot('user-onboarding')
setFlowClient(client)
}
initFlow()
}, [])
return flowClient ? <FlowRenderer client={flowClient} /> : <div>Loading...</div>
}Authentication Flow Implementation
import { makeBasicFlowService } from '@owlmeans/client-flow'
import { STD_AUTH_FLOW } from '@owlmeans/flow'
// Setup authentication flow
const context = makeClientContext({
service: 'my-app',
// ... other config
flowConfig: {
defaultFlow: STD_AUTH_FLOW,
services: {
auth: 'authentication-service',
dashboard: 'user-dashboard'
},
modules: {
login: 'auth-login-module',
register: 'auth-register-module'
}
}
})
const flowService = makeBasicFlowService('auth-flow')
context.registerService(flowService)
// Use in component
function AuthFlow() {
const context = useContext()
const navigator = useNavigate()
const [currentStep, setCurrentStep] = useState(null)
useEffect(() => {
const startAuthFlow = async () => {
const client = createFlowClient(context, navigator)
await client.boot('dashboard', 'login')
const flow = client.flow()
setCurrentStep(flow.step())
}
startAuthFlow()
}, [])
const handleLogin = async (credentials) => {
const client = createFlowClient(context, navigator)
await client.proceed('authenticate', {
body: credentials
})
// Persist progress
await client.persist()
// Update current step
setCurrentStep(client.flow().step())
}
return (
<div>
<h2>Authentication Flow</h2>
<p>Current Step: {currentStep}</p>
{currentStep === 'login' && <LoginForm onSubmit={handleLogin} />}
{currentStep === 'verified' && <Navigate to="/dashboard" />}
</div>
)
}Multi-Step Registration Flow
interface RegistrationData {
personalInfo: { name: string; email: string }
preferences: { theme: string; notifications: boolean }
verification: { code: string }
}
function RegistrationFlow() {
const context = useContext()
const navigator = useNavigate()
const [flowClient, setFlowClient] = useState<FlowClient | null>(null)
const [registrationData, setRegistrationData] = useState<Partial<RegistrationData>>({})
useEffect(() => {
const initRegistration = async () => {
const client = createFlowClient(context, navigator)
await client.boot('user-registration', 'personal-info')
setFlowClient(client)
}
initRegistration()
}, [])
const proceedToNext = async (stepData: any) => {
if (!flowClient) return
// Update registration data
const updatedData = { ...registrationData, ...stepData }
setRegistrationData(updatedData)
// Proceed to next step
await flowClient.proceed('next', {
body: updatedData
})
// Persist state
await flowClient.persist()
}
const getCurrentStep = () => {
return flowClient?.flow().step() || 'loading'
}
const renderCurrentStep = () => {
const step = getCurrentStep()
switch (step) {
case 'personal-info':
return <PersonalInfoForm onSubmit={proceedToNext} />
case 'preferences':
return <PreferencesForm onSubmit={proceedToNext} />
case 'verification':
return <VerificationForm onSubmit={proceedToNext} />
case 'complete':
return <RegistrationComplete />
default:
return <div>Loading...</div>
}
}
return (
<div>
<h2>Registration Flow</h2>
<div>Step: {getCurrentStep()}</div>
{renderCurrentStep()}
</div>
)
}Flow State Persistence
import { appendClientResource } from '@owlmeans/client-resource'
import { FLOW_STATE } from '@owlmeans/client-flow'
// Setup flow state resource
appendClientResource(context, FLOW_STATE)
class FlowStateManager {
constructor(private context: ClientContext) {}
async saveFlowState(flowClient: FlowClient): Promise<boolean> {
try {
const flow = flowClient.flow()
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
await stateResource.save({
id: FLOW_STATE,
flow: flow.name,
step: flow.step(),
service: flowClient.service().service,
data: flow.data(),
timestamp: new Date()
})
return true
} catch (error) {
console.error('Failed to save flow state:', error)
return false
}
}
async loadFlowState(): Promise<StateRecord | null> {
try {
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
return await stateResource.load(FLOW_STATE)
} catch (error) {
console.error('Failed to load flow state:', error)
return null
}
}
async clearFlowState(): Promise<void> {
try {
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
await stateResource.delete(FLOW_STATE)
} catch (error) {
console.error('Failed to clear flow state:', error)
}
}
}
// Usage
const stateManager = new FlowStateManager(context)
// Auto-save flow state
const flowClient = createFlowClient(context, navigator)
await flowClient.boot()
// Save state after each step
await stateManager.saveFlowState(flowClient)
// Restore on application start
const savedState = await stateManager.loadFlowState()
if (savedState) {
const flowService = context.service<FlowService>('flow')
const restoredFlow = await flowService.begin(savedState.flow)
restoredFlow.setState(savedState)
}Custom Flow Service
import { makeBasicFlowService } from '@owlmeans/client-flow'
class CustomFlowService extends makeBasicFlowService('custom') {
async proceed(req?: Partial<AbstractRequest>, dryRun?: boolean): Promise<string> {
if (!this.flow) {
throw new Error('No active flow')
}
const currentStep = this.flow.step()
const transitions = this.flow.transitions()
// Custom logic for determining next step
let nextStep = 'default'
if (req?.body?.action === 'skip') {
nextStep = this.findSkipTransition(transitions)
} else if (req?.body?.action === 'back') {
nextStep = this.findBackTransition(transitions)
} else {
nextStep = this.findNextTransition(transitions, req)
}
if (!dryRun) {
await this.flow.transition(nextStep, req)
}
return nextStep
}
private findNextTransition(transitions: any[], req?: any): string {
// Implement custom transition logic
return transitions[0]?.target || 'end'
}
private findSkipTransition(transitions: any[]): string {
return transitions.find(t => t.type === 'skip')?.target || 'end'
}
private findBackTransition(transitions: any[]): string {
return transitions.find(t => t.type === 'back')?.target || 'start'
}
}
// Register custom service
const customFlowService = new CustomFlowService()
context.registerService(customFlowService)Flow-Aware React Hook
import { useState, useEffect } from 'react'
import { useContext } from '@owlmeans/client'
interface UseFlowResult {
flowClient: FlowClient | null
currentStep: string | null
isLoading: boolean
proceed: (action: string, data?: any) => Promise<void>
persist: () => Promise<boolean>
}
export function useFlow(flowType?: string, targetService?: string): UseFlowResult {
const context = useContext()
const navigator = useNavigate()
const [flowClient, setFlowClient] = useState<FlowClient | null>(null)
const [currentStep, setCurrentStep] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const initFlow = async () => {
try {
const client = createFlowClient(context, navigator)
await client.boot(targetService)
if (flowType) {
const flowService = context.service<FlowService>('flow')
const flow = await flowService.begin(flowType)
client.setup(flow)
}
setFlowClient(client)
setCurrentStep(client.flow().step())
} catch (error) {
console.error('Flow initialization failed:', error)
} finally {
setIsLoading(false)
}
}
initFlow()
}, [flowType, targetService])
const proceed = async (action: string, data?: any) => {
if (!flowClient) return
await flowClient.proceed(action, { body: data })
setCurrentStep(flowClient.flow().step())
}
const persist = async (): Promise<boolean> => {
if (!flowClient) return false
return await flowClient.persist()
}
return {
flowClient,
currentStep,
isLoading,
proceed,
persist
}
}
// Usage in component
function FlowBasedComponent() {
const { currentStep, isLoading, proceed, persist } = useFlow('onboarding', 'dashboard')
if (isLoading) {
return <div>Initializing flow...</div>
}
const handleNext = async (data: any) => {
await proceed('next', data)
await persist()
}
return (
<div>
<h3>Current Step: {currentStep}</h3>
{currentStep === 'welcome' && <WelcomeStep onNext={handleNext} />}
{currentStep === 'setup' && <SetupStep onNext={handleNext} />}
{currentStep === 'complete' && <CompleteStep />}
</div>
)
}Error Handling
The package integrates with the OwlMeans error system and may throw the following errors:
FlowUnsupported
Thrown when an unsupported flow operation is attempted.
UnknownFlow
Thrown when trying to access a flow that hasn't been registered.
FlowTargetError
Thrown when the target service for a flow cannot be resolved.
FlowStepMissconfigured
Thrown when flow steps are not properly configured.
import { FlowUnsupported, UnknownFlow, FlowTargetError } from '@owlmeans/flow'
const handleFlowError = async () => {
try {
await flowClient.proceed('invalid-transition')
} catch (error) {
if (error instanceof FlowUnsupported) {
console.error('Flow operation not supported')
} else if (error instanceof UnknownFlow) {
console.error('Flow not found:', error.message)
} else if (error instanceof FlowTargetError) {
console.error('Invalid flow target:', error.message)
}
}
}Integration with Other Packages
Authentication Integration
import { makeAuthService } from '@owlmeans/client-auth'
import { makeBasicFlowService } from '@owlmeans/client-flow'
// Setup authentication flow
const context = makeClientContext(config)
const authService = makeAuthService()
const flowService = makeBasicFlowService()
context.registerService(authService)
context.registerService(flowService)Resource Integration
import { appendClientResource } from '@owlmeans/client-resource'
import { FLOW_STATE } from '@owlmeans/client-flow'
// Setup flow state persistence
appendClientResource(context, FLOW_STATE)Navigation Integration
import { useNavigate } from '@owlmeans/client'
import { createFlowClient } from '@owlmeans/client-flow'
const navigator = useNavigate()
const flowClient = createFlowClient(context, navigator)Best Practices
- State Persistence: Always persist flow state for long-running processes
- Error Handling: Implement comprehensive error handling for flow operations
- Flow Design: Design flows with clear steps and transition conditions
- Service Integration: Properly configure target services for flows
- React Integration: Use hooks for clean React component integration
- Testing: Test flow transitions and error conditions thoroughly
- Configuration: Use external configuration for flexible flow behavior
Dependencies
This package depends on:
@owlmeans/flow- Core flow interfaces and utilities@owlmeans/client-context- Client context management@owlmeans/client-module- Client module system@owlmeans/client-resource- Client resource management@owlmeans/context- Core context systemreact- React library (peer dependency)
Related Packages
@owlmeans/flow- Core flow system@owlmeans/web-flow- Web-specific flow components@owlmeans/server-flow- Server-side flow processing@owlmeans/client-auth- Authentication with flow integration@owlmeans/client- Base React client library
