@owlmeans/client-flow 0.1.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/LICENSE +21 -0
- package/README.md +877 -0
- package/build/.gitkeep +0 -0
- package/build/client.d.ts +5 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +97 -0
- package/build/client.js.map +1 -0
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +4 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +86 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +34 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +48 -0
- package/src/client.ts +125 -0
- package/src/consts.ts +6 -0
- package/src/index.ts +4 -0
- package/src/service.ts +114 -0
- package/src/types.ts +45 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
# @owlmeans/client-flow
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@owlmeans/client-flow` package extends the base `@owlmeans/flow` package with client-specific functionality. It provides:
|
|
8
|
+
|
|
9
|
+
- **Client Flow Management**: Complete client-side implementation of configurable user flows
|
|
10
|
+
- **State Persistence**: Automatic flow state persistence across browser sessions
|
|
11
|
+
- **Navigation Integration**: Seamless integration with React Router navigation
|
|
12
|
+
- **Authentication Flows**: Pre-built authentication and authorization workflows
|
|
13
|
+
- **Service Integration**: Integration with OwlMeans service and module systems
|
|
14
|
+
- **React Components**: Flow-aware components and hooks for React applications
|
|
15
|
+
- **Configuration Management**: Dynamic flow configuration from external sources
|
|
16
|
+
- **Error Handling**: Robust error handling and recovery mechanisms
|
|
17
|
+
|
|
18
|
+
This package is part of the OwlMeans flow management quadra:
|
|
19
|
+
- **@owlmeans/flow**: Common flow interfaces and utilities
|
|
20
|
+
- **@owlmeans/client-flow**: Client-side flow implementation *(this package)*
|
|
21
|
+
- **@owlmeans/web-flow**: Web-specific flow components and utilities
|
|
22
|
+
- **@owlmeans/server-flow**: Server-side flow processing
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @owlmeans/client-flow react
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Core Concepts
|
|
31
|
+
|
|
32
|
+
### Flow Models
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
### Flow States
|
|
36
|
+
Flow states represent the current position in a workflow and are automatically persisted to survive browser sessions and page reloads.
|
|
37
|
+
|
|
38
|
+
### Flow Service
|
|
39
|
+
The central service that manages flow lifecycle, state transitions, and integration with other application services.
|
|
40
|
+
|
|
41
|
+
### Flow Client
|
|
42
|
+
A client-side wrapper that provides an easy-to-use interface for flow operations with navigation integration.
|
|
43
|
+
|
|
44
|
+
## API Reference
|
|
45
|
+
|
|
46
|
+
### Factory Functions
|
|
47
|
+
|
|
48
|
+
#### `makeBasicFlowService(alias?: string): FlowService`
|
|
49
|
+
|
|
50
|
+
Creates a flow service instance for managing client-side flows.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { makeBasicFlowService } from '@owlmeans/client-flow'
|
|
54
|
+
|
|
55
|
+
const flowService = makeBasicFlowService('user-flows')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Parameters:**
|
|
59
|
+
- `alias`: string (optional) - Service alias for registration, defaults to 'flow'
|
|
60
|
+
|
|
61
|
+
**Returns:** FlowService instance ready for registration with context
|
|
62
|
+
|
|
63
|
+
#### `createFlowClient<C, T>(context: T, nav: Navigator): FlowClient`
|
|
64
|
+
|
|
65
|
+
Creates a flow client that integrates flows with navigation and context.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { createFlowClient } from '@owlmeans/client-flow'
|
|
69
|
+
import { useContext, useNavigate } from '@owlmeans/client'
|
|
70
|
+
|
|
71
|
+
const context = useContext()
|
|
72
|
+
const navigator = useNavigate()
|
|
73
|
+
const flowClient = createFlowClient(context, navigator)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Parameters:**
|
|
77
|
+
- `context`: T - Application context with flow service
|
|
78
|
+
- `nav`: Navigator - Navigation service for routing integration
|
|
79
|
+
|
|
80
|
+
**Returns:** FlowClient instance for flow operations
|
|
81
|
+
|
|
82
|
+
### Core Interfaces
|
|
83
|
+
|
|
84
|
+
#### `FlowService`
|
|
85
|
+
|
|
86
|
+
Main service interface for flow management and lifecycle operations.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
interface FlowService extends LazyService {
|
|
90
|
+
supplied: Promise<boolean> // Flow availability promise
|
|
91
|
+
flow: FlowModel | null // Current active flow
|
|
92
|
+
|
|
93
|
+
// Flow operations
|
|
94
|
+
state(): Promise<FlowModel | null> // Get current flow state
|
|
95
|
+
begin(slug?: string, from?: string): Promise<FlowModel> // Start new flow
|
|
96
|
+
load(flow: string): Promise<FlowModel> // Load existing flow
|
|
97
|
+
proceed(req?, dryRun?): Promise<string> // Proceed to next step
|
|
98
|
+
|
|
99
|
+
// Configuration and providers
|
|
100
|
+
config(): FlowConfig // Get flow configuration
|
|
101
|
+
provideFlow: FlowProvider // Flow provider function
|
|
102
|
+
resolvePair(): ResolvePair // Promise resolution pair
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### `FlowClient`
|
|
107
|
+
|
|
108
|
+
Client-side flow wrapper with navigation and persistence integration.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
interface FlowClient {
|
|
112
|
+
boot(target?: string, from?: string): Promise<FlowClient> // Initialize flow client
|
|
113
|
+
setup(flow: FlowModel): FlowClient // Setup with existing flow
|
|
114
|
+
flow(): FlowModel // Get current flow model
|
|
115
|
+
service(): ResolvedServiceRoute // Get target service route
|
|
116
|
+
proceed(transition: FlowTransition, req?): Promise<void> // Execute transition
|
|
117
|
+
persist(): Promise<boolean> // Persist current state
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `StateRecord`
|
|
122
|
+
|
|
123
|
+
Interface for flow state persistence records.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
interface StateRecord extends ResourceRecord, FlowState {
|
|
127
|
+
id: string // Record identifier
|
|
128
|
+
flow: string // Flow type identifier
|
|
129
|
+
step: string // Current step identifier
|
|
130
|
+
service: string // Target service
|
|
131
|
+
data: any // Flow-specific data
|
|
132
|
+
// ... other FlowState properties
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### `StateResource`
|
|
137
|
+
|
|
138
|
+
Resource interface for managing flow state persistence.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface StateResource extends ClientResource<StateRecord> {
|
|
142
|
+
// Inherits all standard resource methods for CRUD operations
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Flow Service Methods Detailed Reference
|
|
147
|
+
|
|
148
|
+
#### `state(): Promise<FlowModel | null>`
|
|
149
|
+
|
|
150
|
+
**Purpose**: Retrieves the current active flow model
|
|
151
|
+
|
|
152
|
+
**Behavior**:
|
|
153
|
+
- Waits for flow service to be supplied/ready
|
|
154
|
+
- Returns the currently active flow or null if no flow is active
|
|
155
|
+
- Used to check flow status before operations
|
|
156
|
+
|
|
157
|
+
**Usage**: Checking if a flow is currently active
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const flowService = context.service<FlowService>('flow')
|
|
161
|
+
|
|
162
|
+
const currentFlow = await flowService.state()
|
|
163
|
+
if (currentFlow) {
|
|
164
|
+
console.log('Active flow:', currentFlow.name)
|
|
165
|
+
console.log('Current step:', currentFlow.step())
|
|
166
|
+
} else {
|
|
167
|
+
console.log('No active flow')
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `begin(slug?: string, from?: string): Promise<FlowModel>`
|
|
172
|
+
|
|
173
|
+
**Purpose**: Starts a new flow with the specified configuration
|
|
174
|
+
|
|
175
|
+
**Behavior**:
|
|
176
|
+
- Creates a new flow model based on the slug
|
|
177
|
+
- Sets the service as supplied/ready
|
|
178
|
+
- Enters the flow from the specified step or initial step
|
|
179
|
+
- Returns the active flow model
|
|
180
|
+
|
|
181
|
+
**Usage**: Starting authentication, registration, or other workflows
|
|
182
|
+
|
|
183
|
+
**Parameters**:
|
|
184
|
+
- `slug`: string (optional) - Flow identifier, defaults to config.defaultFlow or STD_AUTH_FLOW
|
|
185
|
+
- `from`: string (optional) - Starting step, defaults to flow's initial step
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const flowService = context.service<FlowService>('flow')
|
|
189
|
+
|
|
190
|
+
// Start default authentication flow
|
|
191
|
+
const authFlow = await flowService.begin()
|
|
192
|
+
|
|
193
|
+
// Start specific flow from specific step
|
|
194
|
+
const registrationFlow = await flowService.begin('user-registration', 'email-verification')
|
|
195
|
+
|
|
196
|
+
// Start flow with default slug
|
|
197
|
+
const defaultFlow = await flowService.begin('custom-onboarding')
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `load(flow: string): Promise<FlowModel>`
|
|
201
|
+
|
|
202
|
+
**Purpose**: Loads and activates a flow from serialized state
|
|
203
|
+
|
|
204
|
+
**Behavior**:
|
|
205
|
+
- Recreates flow model from serialized state string
|
|
206
|
+
- Sets the service as supplied/ready
|
|
207
|
+
- Restores flow to its previous state
|
|
208
|
+
- Returns the restored flow model
|
|
209
|
+
|
|
210
|
+
**Usage**: Restoring flows from persistent storage
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const flowService = context.service<FlowService>('flow')
|
|
214
|
+
|
|
215
|
+
// Load flow from saved state
|
|
216
|
+
const savedFlowState = localStorage.getItem('flow-state')
|
|
217
|
+
if (savedFlowState) {
|
|
218
|
+
const restoredFlow = await flowService.load(savedFlowState)
|
|
219
|
+
console.log('Flow restored to step:', restoredFlow.step())
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `proceed(req?: Partial<AbstractRequest>, dryRun?: boolean): Promise<string>`
|
|
224
|
+
|
|
225
|
+
**Purpose**: Advances the flow to the next step based on conditions
|
|
226
|
+
|
|
227
|
+
**Behavior**:
|
|
228
|
+
- Evaluates current step's transitions and conditions
|
|
229
|
+
- Moves to the next appropriate step
|
|
230
|
+
- Throws FlowUnsupported error (not implemented in basic service)
|
|
231
|
+
- Should be overridden in specific implementations
|
|
232
|
+
|
|
233
|
+
**Usage**: Advancing through flow steps
|
|
234
|
+
|
|
235
|
+
**Note**: The basic service throws `FlowUnsupported` - this should be implemented by specific flow implementations
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// This would be implemented in a specific flow service
|
|
239
|
+
const nextStep = await flowService.proceed({
|
|
240
|
+
body: { userInput: 'some-data' }
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `config(): FlowConfig`
|
|
245
|
+
|
|
246
|
+
**Purpose**: Retrieves the current flow configuration
|
|
247
|
+
|
|
248
|
+
**Behavior**:
|
|
249
|
+
- Accesses flow configuration from context
|
|
250
|
+
- Returns default empty config if not configured
|
|
251
|
+
- Used for flow behavior customization
|
|
252
|
+
|
|
253
|
+
**Usage**: Accessing flow settings and configuration
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const flowService = context.service<FlowService>('flow')
|
|
257
|
+
|
|
258
|
+
const config = flowService.config()
|
|
259
|
+
console.log('Default flow:', config.defaultFlow)
|
|
260
|
+
console.log('Available services:', config.services)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Flow Client Methods Detailed Reference
|
|
264
|
+
|
|
265
|
+
#### `boot(target?: string, from?: string): Promise<FlowClient>`
|
|
266
|
+
|
|
267
|
+
**Purpose**: Initializes the flow client with optional target service and starting point
|
|
268
|
+
|
|
269
|
+
**Behavior**:
|
|
270
|
+
- Waits for flow service to be ready
|
|
271
|
+
- Attempts to restore flow from persistent storage
|
|
272
|
+
- Creates new flow if no saved state exists
|
|
273
|
+
- Sets target service if provided
|
|
274
|
+
- Returns the initialized client
|
|
275
|
+
|
|
276
|
+
**Usage**: Starting the flow system in your application
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const flowClient = createFlowClient(context, navigator)
|
|
280
|
+
|
|
281
|
+
// Boot with specific target service
|
|
282
|
+
await flowClient.boot('user-dashboard', 'login-step')
|
|
283
|
+
|
|
284
|
+
// Boot with default behavior
|
|
285
|
+
await flowClient.boot()
|
|
286
|
+
|
|
287
|
+
// Boot without target, from saved state
|
|
288
|
+
await flowClient.boot(null)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### `setup(flow: FlowModel): FlowClient`
|
|
292
|
+
|
|
293
|
+
**Purpose**: Configures the client with an existing flow model
|
|
294
|
+
|
|
295
|
+
**Behavior**:
|
|
296
|
+
- Sets the provided flow as the active flow
|
|
297
|
+
- Returns the client for method chaining
|
|
298
|
+
- Used when you already have a flow model
|
|
299
|
+
|
|
300
|
+
**Usage**: Using pre-configured flow models
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const existingFlow = await flowService.begin('checkout-flow')
|
|
304
|
+
const flowClient = createFlowClient(context, navigator)
|
|
305
|
+
.setup(existingFlow)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `flow(): FlowModel`
|
|
309
|
+
|
|
310
|
+
**Purpose**: Returns the current active flow model
|
|
311
|
+
|
|
312
|
+
**Behavior**: Direct access to the underlying flow model
|
|
313
|
+
|
|
314
|
+
**Usage**: Accessing flow state and methods
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const currentFlow = flowClient.flow()
|
|
318
|
+
console.log('Current step:', currentFlow.step())
|
|
319
|
+
console.log('Flow data:', currentFlow.data())
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### `service(): ResolvedServiceRoute`
|
|
323
|
+
|
|
324
|
+
**Purpose**: Returns the resolved service route for the target service
|
|
325
|
+
|
|
326
|
+
**Behavior**:
|
|
327
|
+
- Resolves the target service from the context
|
|
328
|
+
- Returns route information for navigation
|
|
329
|
+
|
|
330
|
+
**Usage**: Getting service routing information
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const serviceRoute = flowClient.service()
|
|
334
|
+
console.log('Target service:', serviceRoute.service)
|
|
335
|
+
console.log('Service path:', serviceRoute.path)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `proceed(transition: FlowTransition, req?: Partial<AbstractRequest>): Promise<void>`
|
|
339
|
+
|
|
340
|
+
**Purpose**: Executes a flow transition with optional request data
|
|
341
|
+
|
|
342
|
+
**Behavior**:
|
|
343
|
+
- Executes the specified transition
|
|
344
|
+
- Passes request data to the transition handler
|
|
345
|
+
- Updates flow state based on transition result
|
|
346
|
+
- Handles navigation if required
|
|
347
|
+
|
|
348
|
+
**Usage**: Moving between flow steps
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
await flowClient.proceed('next', {
|
|
352
|
+
body: { formData: userData }
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await flowClient.proceed('back')
|
|
356
|
+
|
|
357
|
+
await flowClient.proceed('submit', {
|
|
358
|
+
params: { id: userId }
|
|
359
|
+
})
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `persist(): Promise<boolean>`
|
|
363
|
+
|
|
364
|
+
**Purpose**: Saves the current flow state to persistent storage
|
|
365
|
+
|
|
366
|
+
**Behavior**:
|
|
367
|
+
- Serializes current flow state
|
|
368
|
+
- Saves to flow state resource
|
|
369
|
+
- Returns success status
|
|
370
|
+
|
|
371
|
+
**Usage**: Saving flow progress
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const saved = await flowClient.persist()
|
|
375
|
+
if (saved) {
|
|
376
|
+
console.log('Flow state saved successfully')
|
|
377
|
+
} else {
|
|
378
|
+
console.log('Failed to save flow state')
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Constants
|
|
383
|
+
|
|
384
|
+
#### `DEFAULT_ALIAS`
|
|
385
|
+
Default flow service alias (`'flow'`).
|
|
386
|
+
|
|
387
|
+
#### `FLOW_STATE`
|
|
388
|
+
Resource identifier for flow state persistence (`'state:flow'`).
|
|
389
|
+
|
|
390
|
+
#### `REHACK_MOD`
|
|
391
|
+
Internal module identifier for redirects (`'__redirect'`).
|
|
392
|
+
|
|
393
|
+
## Usage Examples
|
|
394
|
+
|
|
395
|
+
### Basic Flow Setup
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { makeBasicFlowService, createFlowClient } from '@owlmeans/client-flow'
|
|
399
|
+
import { makeClientContext, useContext } from '@owlmeans/client'
|
|
400
|
+
|
|
401
|
+
// Create context with flow service
|
|
402
|
+
const context = makeClientContext(config)
|
|
403
|
+
const flowService = makeBasicFlowService()
|
|
404
|
+
context.registerService(flowService)
|
|
405
|
+
|
|
406
|
+
// Initialize context
|
|
407
|
+
await context.configure().init()
|
|
408
|
+
|
|
409
|
+
// Create flow client
|
|
410
|
+
function MyFlowComponent() {
|
|
411
|
+
const context = useContext()
|
|
412
|
+
const navigator = useNavigate()
|
|
413
|
+
|
|
414
|
+
const [flowClient, setFlowClient] = useState(null)
|
|
415
|
+
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
const initFlow = async () => {
|
|
418
|
+
const client = createFlowClient(context, navigator)
|
|
419
|
+
await client.boot('user-onboarding')
|
|
420
|
+
setFlowClient(client)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
initFlow()
|
|
424
|
+
}, [])
|
|
425
|
+
|
|
426
|
+
return flowClient ? <FlowRenderer client={flowClient} /> : <div>Loading...</div>
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Authentication Flow Implementation
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { makeBasicFlowService } from '@owlmeans/client-flow'
|
|
434
|
+
import { STD_AUTH_FLOW } from '@owlmeans/flow'
|
|
435
|
+
|
|
436
|
+
// Setup authentication flow
|
|
437
|
+
const context = makeClientContext({
|
|
438
|
+
service: 'my-app',
|
|
439
|
+
// ... other config
|
|
440
|
+
flowConfig: {
|
|
441
|
+
defaultFlow: STD_AUTH_FLOW,
|
|
442
|
+
services: {
|
|
443
|
+
auth: 'authentication-service',
|
|
444
|
+
dashboard: 'user-dashboard'
|
|
445
|
+
},
|
|
446
|
+
modules: {
|
|
447
|
+
login: 'auth-login-module',
|
|
448
|
+
register: 'auth-register-module'
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const flowService = makeBasicFlowService('auth-flow')
|
|
454
|
+
context.registerService(flowService)
|
|
455
|
+
|
|
456
|
+
// Use in component
|
|
457
|
+
function AuthFlow() {
|
|
458
|
+
const context = useContext()
|
|
459
|
+
const navigator = useNavigate()
|
|
460
|
+
const [currentStep, setCurrentStep] = useState(null)
|
|
461
|
+
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
const startAuthFlow = async () => {
|
|
464
|
+
const client = createFlowClient(context, navigator)
|
|
465
|
+
await client.boot('dashboard', 'login')
|
|
466
|
+
|
|
467
|
+
const flow = client.flow()
|
|
468
|
+
setCurrentStep(flow.step())
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
startAuthFlow()
|
|
472
|
+
}, [])
|
|
473
|
+
|
|
474
|
+
const handleLogin = async (credentials) => {
|
|
475
|
+
const client = createFlowClient(context, navigator)
|
|
476
|
+
await client.proceed('authenticate', {
|
|
477
|
+
body: credentials
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// Persist progress
|
|
481
|
+
await client.persist()
|
|
482
|
+
|
|
483
|
+
// Update current step
|
|
484
|
+
setCurrentStep(client.flow().step())
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<div>
|
|
489
|
+
<h2>Authentication Flow</h2>
|
|
490
|
+
<p>Current Step: {currentStep}</p>
|
|
491
|
+
{currentStep === 'login' && <LoginForm onSubmit={handleLogin} />}
|
|
492
|
+
{currentStep === 'verified' && <Navigate to="/dashboard" />}
|
|
493
|
+
</div>
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Multi-Step Registration Flow
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
interface RegistrationData {
|
|
502
|
+
personalInfo: { name: string; email: string }
|
|
503
|
+
preferences: { theme: string; notifications: boolean }
|
|
504
|
+
verification: { code: string }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function RegistrationFlow() {
|
|
508
|
+
const context = useContext()
|
|
509
|
+
const navigator = useNavigate()
|
|
510
|
+
const [flowClient, setFlowClient] = useState<FlowClient | null>(null)
|
|
511
|
+
const [registrationData, setRegistrationData] = useState<Partial<RegistrationData>>({})
|
|
512
|
+
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
const initRegistration = async () => {
|
|
515
|
+
const client = createFlowClient(context, navigator)
|
|
516
|
+
await client.boot('user-registration', 'personal-info')
|
|
517
|
+
setFlowClient(client)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
initRegistration()
|
|
521
|
+
}, [])
|
|
522
|
+
|
|
523
|
+
const proceedToNext = async (stepData: any) => {
|
|
524
|
+
if (!flowClient) return
|
|
525
|
+
|
|
526
|
+
// Update registration data
|
|
527
|
+
const updatedData = { ...registrationData, ...stepData }
|
|
528
|
+
setRegistrationData(updatedData)
|
|
529
|
+
|
|
530
|
+
// Proceed to next step
|
|
531
|
+
await flowClient.proceed('next', {
|
|
532
|
+
body: updatedData
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// Persist state
|
|
536
|
+
await flowClient.persist()
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const getCurrentStep = () => {
|
|
540
|
+
return flowClient?.flow().step() || 'loading'
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const renderCurrentStep = () => {
|
|
544
|
+
const step = getCurrentStep()
|
|
545
|
+
|
|
546
|
+
switch (step) {
|
|
547
|
+
case 'personal-info':
|
|
548
|
+
return <PersonalInfoForm onSubmit={proceedToNext} />
|
|
549
|
+
case 'preferences':
|
|
550
|
+
return <PreferencesForm onSubmit={proceedToNext} />
|
|
551
|
+
case 'verification':
|
|
552
|
+
return <VerificationForm onSubmit={proceedToNext} />
|
|
553
|
+
case 'complete':
|
|
554
|
+
return <RegistrationComplete />
|
|
555
|
+
default:
|
|
556
|
+
return <div>Loading...</div>
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<div>
|
|
562
|
+
<h2>Registration Flow</h2>
|
|
563
|
+
<div>Step: {getCurrentStep()}</div>
|
|
564
|
+
{renderCurrentStep()}
|
|
565
|
+
</div>
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Flow State Persistence
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
import { appendClientResource } from '@owlmeans/client-resource'
|
|
574
|
+
import { FLOW_STATE } from '@owlmeans/client-flow'
|
|
575
|
+
|
|
576
|
+
// Setup flow state resource
|
|
577
|
+
appendClientResource(context, FLOW_STATE)
|
|
578
|
+
|
|
579
|
+
class FlowStateManager {
|
|
580
|
+
constructor(private context: ClientContext) {}
|
|
581
|
+
|
|
582
|
+
async saveFlowState(flowClient: FlowClient): Promise<boolean> {
|
|
583
|
+
try {
|
|
584
|
+
const flow = flowClient.flow()
|
|
585
|
+
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
|
|
586
|
+
|
|
587
|
+
await stateResource.save({
|
|
588
|
+
id: FLOW_STATE,
|
|
589
|
+
flow: flow.name,
|
|
590
|
+
step: flow.step(),
|
|
591
|
+
service: flowClient.service().service,
|
|
592
|
+
data: flow.data(),
|
|
593
|
+
timestamp: new Date()
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
return true
|
|
597
|
+
} catch (error) {
|
|
598
|
+
console.error('Failed to save flow state:', error)
|
|
599
|
+
return false
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async loadFlowState(): Promise<StateRecord | null> {
|
|
604
|
+
try {
|
|
605
|
+
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
|
|
606
|
+
return await stateResource.load(FLOW_STATE)
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.error('Failed to load flow state:', error)
|
|
609
|
+
return null
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async clearFlowState(): Promise<void> {
|
|
614
|
+
try {
|
|
615
|
+
const stateResource = this.context.resource<StateResource>(FLOW_STATE)
|
|
616
|
+
await stateResource.delete(FLOW_STATE)
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error('Failed to clear flow state:', error)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Usage
|
|
624
|
+
const stateManager = new FlowStateManager(context)
|
|
625
|
+
|
|
626
|
+
// Auto-save flow state
|
|
627
|
+
const flowClient = createFlowClient(context, navigator)
|
|
628
|
+
await flowClient.boot()
|
|
629
|
+
|
|
630
|
+
// Save state after each step
|
|
631
|
+
await stateManager.saveFlowState(flowClient)
|
|
632
|
+
|
|
633
|
+
// Restore on application start
|
|
634
|
+
const savedState = await stateManager.loadFlowState()
|
|
635
|
+
if (savedState) {
|
|
636
|
+
const flowService = context.service<FlowService>('flow')
|
|
637
|
+
const restoredFlow = await flowService.begin(savedState.flow)
|
|
638
|
+
restoredFlow.setState(savedState)
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Custom Flow Service
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { makeBasicFlowService } from '@owlmeans/client-flow'
|
|
646
|
+
|
|
647
|
+
class CustomFlowService extends makeBasicFlowService('custom') {
|
|
648
|
+
async proceed(req?: Partial<AbstractRequest>, dryRun?: boolean): Promise<string> {
|
|
649
|
+
if (!this.flow) {
|
|
650
|
+
throw new Error('No active flow')
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const currentStep = this.flow.step()
|
|
654
|
+
const transitions = this.flow.transitions()
|
|
655
|
+
|
|
656
|
+
// Custom logic for determining next step
|
|
657
|
+
let nextStep = 'default'
|
|
658
|
+
|
|
659
|
+
if (req?.body?.action === 'skip') {
|
|
660
|
+
nextStep = this.findSkipTransition(transitions)
|
|
661
|
+
} else if (req?.body?.action === 'back') {
|
|
662
|
+
nextStep = this.findBackTransition(transitions)
|
|
663
|
+
} else {
|
|
664
|
+
nextStep = this.findNextTransition(transitions, req)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!dryRun) {
|
|
668
|
+
await this.flow.transition(nextStep, req)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return nextStep
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private findNextTransition(transitions: any[], req?: any): string {
|
|
675
|
+
// Implement custom transition logic
|
|
676
|
+
return transitions[0]?.target || 'end'
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private findSkipTransition(transitions: any[]): string {
|
|
680
|
+
return transitions.find(t => t.type === 'skip')?.target || 'end'
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private findBackTransition(transitions: any[]): string {
|
|
684
|
+
return transitions.find(t => t.type === 'back')?.target || 'start'
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Register custom service
|
|
689
|
+
const customFlowService = new CustomFlowService()
|
|
690
|
+
context.registerService(customFlowService)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Flow-Aware React Hook
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
import { useState, useEffect } from 'react'
|
|
697
|
+
import { useContext } from '@owlmeans/client'
|
|
698
|
+
|
|
699
|
+
interface UseFlowResult {
|
|
700
|
+
flowClient: FlowClient | null
|
|
701
|
+
currentStep: string | null
|
|
702
|
+
isLoading: boolean
|
|
703
|
+
proceed: (action: string, data?: any) => Promise<void>
|
|
704
|
+
persist: () => Promise<boolean>
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function useFlow(flowType?: string, targetService?: string): UseFlowResult {
|
|
708
|
+
const context = useContext()
|
|
709
|
+
const navigator = useNavigate()
|
|
710
|
+
const [flowClient, setFlowClient] = useState<FlowClient | null>(null)
|
|
711
|
+
const [currentStep, setCurrentStep] = useState<string | null>(null)
|
|
712
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
713
|
+
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
const initFlow = async () => {
|
|
716
|
+
try {
|
|
717
|
+
const client = createFlowClient(context, navigator)
|
|
718
|
+
await client.boot(targetService)
|
|
719
|
+
|
|
720
|
+
if (flowType) {
|
|
721
|
+
const flowService = context.service<FlowService>('flow')
|
|
722
|
+
const flow = await flowService.begin(flowType)
|
|
723
|
+
client.setup(flow)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
setFlowClient(client)
|
|
727
|
+
setCurrentStep(client.flow().step())
|
|
728
|
+
} catch (error) {
|
|
729
|
+
console.error('Flow initialization failed:', error)
|
|
730
|
+
} finally {
|
|
731
|
+
setIsLoading(false)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
initFlow()
|
|
736
|
+
}, [flowType, targetService])
|
|
737
|
+
|
|
738
|
+
const proceed = async (action: string, data?: any) => {
|
|
739
|
+
if (!flowClient) return
|
|
740
|
+
|
|
741
|
+
await flowClient.proceed(action, { body: data })
|
|
742
|
+
setCurrentStep(flowClient.flow().step())
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const persist = async (): Promise<boolean> => {
|
|
746
|
+
if (!flowClient) return false
|
|
747
|
+
return await flowClient.persist()
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
flowClient,
|
|
752
|
+
currentStep,
|
|
753
|
+
isLoading,
|
|
754
|
+
proceed,
|
|
755
|
+
persist
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Usage in component
|
|
760
|
+
function FlowBasedComponent() {
|
|
761
|
+
const { currentStep, isLoading, proceed, persist } = useFlow('onboarding', 'dashboard')
|
|
762
|
+
|
|
763
|
+
if (isLoading) {
|
|
764
|
+
return <div>Initializing flow...</div>
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const handleNext = async (data: any) => {
|
|
768
|
+
await proceed('next', data)
|
|
769
|
+
await persist()
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
<div>
|
|
774
|
+
<h3>Current Step: {currentStep}</h3>
|
|
775
|
+
{currentStep === 'welcome' && <WelcomeStep onNext={handleNext} />}
|
|
776
|
+
{currentStep === 'setup' && <SetupStep onNext={handleNext} />}
|
|
777
|
+
{currentStep === 'complete' && <CompleteStep />}
|
|
778
|
+
</div>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
## Error Handling
|
|
784
|
+
|
|
785
|
+
The package integrates with the OwlMeans error system and may throw the following errors:
|
|
786
|
+
|
|
787
|
+
### `FlowUnsupported`
|
|
788
|
+
Thrown when an unsupported flow operation is attempted.
|
|
789
|
+
|
|
790
|
+
### `UnknownFlow`
|
|
791
|
+
Thrown when trying to access a flow that hasn't been registered.
|
|
792
|
+
|
|
793
|
+
### `FlowTargetError`
|
|
794
|
+
Thrown when the target service for a flow cannot be resolved.
|
|
795
|
+
|
|
796
|
+
### `FlowStepMissconfigured`
|
|
797
|
+
Thrown when flow steps are not properly configured.
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
import { FlowUnsupported, UnknownFlow, FlowTargetError } from '@owlmeans/flow'
|
|
801
|
+
|
|
802
|
+
const handleFlowError = async () => {
|
|
803
|
+
try {
|
|
804
|
+
await flowClient.proceed('invalid-transition')
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (error instanceof FlowUnsupported) {
|
|
807
|
+
console.error('Flow operation not supported')
|
|
808
|
+
} else if (error instanceof UnknownFlow) {
|
|
809
|
+
console.error('Flow not found:', error.message)
|
|
810
|
+
} else if (error instanceof FlowTargetError) {
|
|
811
|
+
console.error('Invalid flow target:', error.message)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
## Integration with Other Packages
|
|
818
|
+
|
|
819
|
+
### Authentication Integration
|
|
820
|
+
```typescript
|
|
821
|
+
import { makeAuthService } from '@owlmeans/client-auth'
|
|
822
|
+
import { makeBasicFlowService } from '@owlmeans/client-flow'
|
|
823
|
+
|
|
824
|
+
// Setup authentication flow
|
|
825
|
+
const context = makeClientContext(config)
|
|
826
|
+
const authService = makeAuthService()
|
|
827
|
+
const flowService = makeBasicFlowService()
|
|
828
|
+
|
|
829
|
+
context.registerService(authService)
|
|
830
|
+
context.registerService(flowService)
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Resource Integration
|
|
834
|
+
```typescript
|
|
835
|
+
import { appendClientResource } from '@owlmeans/client-resource'
|
|
836
|
+
import { FLOW_STATE } from '@owlmeans/client-flow'
|
|
837
|
+
|
|
838
|
+
// Setup flow state persistence
|
|
839
|
+
appendClientResource(context, FLOW_STATE)
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Navigation Integration
|
|
843
|
+
```typescript
|
|
844
|
+
import { useNavigate } from '@owlmeans/client'
|
|
845
|
+
import { createFlowClient } from '@owlmeans/client-flow'
|
|
846
|
+
|
|
847
|
+
const navigator = useNavigate()
|
|
848
|
+
const flowClient = createFlowClient(context, navigator)
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
## Best Practices
|
|
852
|
+
|
|
853
|
+
1. **State Persistence**: Always persist flow state for long-running processes
|
|
854
|
+
2. **Error Handling**: Implement comprehensive error handling for flow operations
|
|
855
|
+
3. **Flow Design**: Design flows with clear steps and transition conditions
|
|
856
|
+
4. **Service Integration**: Properly configure target services for flows
|
|
857
|
+
5. **React Integration**: Use hooks for clean React component integration
|
|
858
|
+
6. **Testing**: Test flow transitions and error conditions thoroughly
|
|
859
|
+
7. **Configuration**: Use external configuration for flexible flow behavior
|
|
860
|
+
|
|
861
|
+
## Dependencies
|
|
862
|
+
|
|
863
|
+
This package depends on:
|
|
864
|
+
- `@owlmeans/flow` - Core flow interfaces and utilities
|
|
865
|
+
- `@owlmeans/client-context` - Client context management
|
|
866
|
+
- `@owlmeans/client-module` - Client module system
|
|
867
|
+
- `@owlmeans/client-resource` - Client resource management
|
|
868
|
+
- `@owlmeans/context` - Core context system
|
|
869
|
+
- `react` - React library (peer dependency)
|
|
870
|
+
|
|
871
|
+
## Related Packages
|
|
872
|
+
|
|
873
|
+
- [`@owlmeans/flow`](../flow) - Core flow system
|
|
874
|
+
- [`@owlmeans/web-flow`](../web-flow) - Web-specific flow components
|
|
875
|
+
- [`@owlmeans/server-flow`](../server-flow) - Server-side flow processing
|
|
876
|
+
- [`@owlmeans/client-auth`](../client-auth) - Authentication with flow integration
|
|
877
|
+
- [`@owlmeans/client`](../client) - Base React client library
|