@owlmeans/web-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # @owlmeans/web-flow
2
+
3
+ Web-specific flow management library for OwlMeans Common applications. This package extends `@owlmeans/client-flow` with web browser-specific functionality including URL-based flow state management, browser navigation integration, and query parameter handling.
4
+
5
+ ## Overview
6
+
7
+ The `@owlmeans/web-flow` package provides web-specific implementations of the OwlMeans flow system. It offers:
8
+
9
+ - **URL-Based Flow State**: Flow state management through browser URLs and query parameters
10
+ - **Browser Navigation**: Integration with browser history and navigation APIs
11
+ - **Resource Persistence**: Web-specific flow state persistence using browser storage
12
+ - **Query Parameter Management**: Automatic flow state serialization in URL parameters
13
+ - **Module Integration**: Enhanced module integration for web-based flow transitions
14
+ - **Client-Side Routing**: Seamless integration with React Router for flow navigation
15
+
16
+ This package is part of the OwlMeans flow management quadra:
17
+ - **@owlmeans/flow**: Common flow interfaces and utilities
18
+ - **@owlmeans/client-flow**: Client-side flow implementation
19
+ - **@owlmeans/web-flow**: Web-specific flow implementation *(this package)*
20
+ - **@owlmeans/server-flow**: Server-side flow processing
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @owlmeans/web-flow react react-router-dom
26
+ ```
27
+
28
+ ## Core Concepts
29
+
30
+ ### Web Flow Service
31
+ Extends the basic flow service with web-specific capabilities like URL management and browser navigation.
32
+
33
+ ### URL State Management
34
+ Flow state is automatically serialized and stored in URL query parameters, allowing flows to survive page reloads and be shareable.
35
+
36
+ ### Browser Integration
37
+ Full integration with browser APIs for navigation, history management, and URL manipulation.
38
+
39
+ ## API Reference
40
+
41
+ ### Factory Functions
42
+
43
+ #### `makeFlowService(alias?: string): FlowService`
44
+
45
+ Creates a web-specific flow service with enhanced browser integration.
46
+
47
+ ```typescript
48
+ import { makeFlowService } from '@owlmeans/web-flow'
49
+
50
+ const webFlowService = makeFlowService('web-flow')
51
+ ```
52
+
53
+ **Parameters:**
54
+ - `alias`: string (optional) - Service alias for registration, defaults to 'flow'
55
+
56
+ **Returns:** Enhanced FlowService with web-specific capabilities
57
+
58
+ #### `appendWebFlowService<C, T>(context: T, alias?: string): T`
59
+
60
+ Appends a web flow service to the application context with necessary resource setup.
61
+
62
+ ```typescript
63
+ import { appendWebFlowService } from '@owlmeans/web-flow'
64
+ import { makeClientContext } from '@owlmeans/client-context'
65
+
66
+ const context = makeClientContext(config)
67
+ appendWebFlowService(context)
68
+ ```
69
+
70
+ **Parameters:**
71
+ - `context`: T - The client context to append the service to
72
+ - `alias`: string (optional) - Service alias, defaults to 'flow'
73
+
74
+ **Returns:** Enhanced context with web flow service and resources
75
+
76
+ ### Enhanced Methods
77
+
78
+ #### `proceed(req?: Partial<AbstractRequest>, dryRun?: boolean): Promise<string>`
79
+
80
+ Web-specific implementation of flow progression with URL management.
81
+
82
+ **Behavior**:
83
+ - Calls the appropriate module for the current flow step
84
+ - Serializes flow state into URL query parameters
85
+ - Handles browser navigation and URL updates
86
+ - Integrates with React Router for seamless transitions
87
+
88
+ **Usage**: Advancing through flow steps with automatic URL updates
89
+
90
+ ```typescript
91
+ const webFlowService = context.service<FlowService>('flow')
92
+
93
+ // Proceed to next step with automatic URL update
94
+ await webFlowService.proceed({
95
+ body: { userInput: 'data' }
96
+ })
97
+ ```
98
+
99
+ ### Configuration
100
+
101
+ #### Flow Configuration Options
102
+
103
+ ```typescript
104
+ interface WebFlowConfig extends FlowConfig {
105
+ queryParam?: string // URL query parameter name for flow state
106
+ }
107
+ ```
108
+
109
+ ### Constants
110
+
111
+ #### `QUERY_PARAM`
112
+ Default query parameter name for flow state (`'flow'`).
113
+
114
+ ## Usage Examples
115
+
116
+ ### Basic Web Flow Setup
117
+
118
+ ```typescript
119
+ import { appendWebFlowService } from '@owlmeans/web-flow'
120
+ import { makeClientContext } from '@owlmeans/client-context'
121
+
122
+ // Create context with web flow
123
+ const context = makeClientContext({
124
+ service: 'my-web-app',
125
+ flowConfig: {
126
+ defaultFlow: 'user-onboarding',
127
+ queryParam: 'state' // Custom query parameter name
128
+ }
129
+ })
130
+
131
+ appendWebFlowService(context)
132
+
133
+ // Initialize
134
+ await context.configure().init()
135
+ ```
136
+
137
+ ### URL-Aware Flow Component
138
+
139
+ ```typescript
140
+ import { useContext } from '@owlmeans/client'
141
+ import { useSearchParams } from 'react-router-dom'
142
+ import { useEffect, useState } from 'react'
143
+
144
+ function WebFlowComponent() {
145
+ const context = useContext()
146
+ const [searchParams] = useSearchParams()
147
+ const [currentStep, setCurrentStep] = useState(null)
148
+
149
+ useEffect(() => {
150
+ const initFlow = async () => {
151
+ const flowService = context.service('flow')
152
+
153
+ // Check if flow state exists in URL
154
+ const flowState = searchParams.get('flow')
155
+ let flow
156
+
157
+ if (flowState) {
158
+ // Restore flow from URL parameter
159
+ flow = await flowService.load(flowState)
160
+ } else {
161
+ // Start new flow
162
+ flow = await flowService.begin('user-onboarding')
163
+ }
164
+
165
+ setCurrentStep(flow.step())
166
+ }
167
+
168
+ initFlow()
169
+ }, [searchParams])
170
+
171
+ const handleNext = async () => {
172
+ const flowService = context.service('flow')
173
+ await flowService.proceed({ action: 'next' })
174
+
175
+ // Flow state is automatically updated in URL
176
+ const updatedFlow = await flowService.state()
177
+ setCurrentStep(updatedFlow?.step())
178
+ }
179
+
180
+ return (
181
+ <div>
182
+ <h2>Current Step: {currentStep}</h2>
183
+ <button onClick={handleNext}>Next Step</button>
184
+ </div>
185
+ )
186
+ }
187
+ ```
188
+
189
+ ### Shareable Flow URLs
190
+
191
+ ```typescript
192
+ function ShareableFlow() {
193
+ const context = useContext()
194
+
195
+ const getShareableURL = async () => {
196
+ const flowService = context.service('flow')
197
+ const flow = await flowService.state()
198
+
199
+ if (flow) {
200
+ const currentURL = new URL(window.location.href)
201
+ currentURL.searchParams.set('flow', flow.serialize())
202
+ return currentURL.toString()
203
+ }
204
+
205
+ return window.location.href
206
+ }
207
+
208
+ const handleShare = async () => {
209
+ const shareableURL = await getShareableURL()
210
+
211
+ if (navigator.share) {
212
+ await navigator.share({
213
+ title: 'Continue Flow',
214
+ url: shareableURL
215
+ })
216
+ } else {
217
+ // Fallback to clipboard
218
+ await navigator.clipboard.writeText(shareableURL)
219
+ alert('URL copied to clipboard!')
220
+ }
221
+ }
222
+
223
+ return (
224
+ <button onClick={handleShare}>
225
+ Share Flow Progress
226
+ </button>
227
+ )
228
+ }
229
+ ```
230
+
231
+ ### Browser Navigation Integration
232
+
233
+ ```typescript
234
+ import { useNavigate, useLocation } from 'react-router-dom'
235
+ import { useContext } from '@owlmeans/client'
236
+
237
+ function NavigationAwareFlow() {
238
+ const context = useContext()
239
+ const navigate = useNavigate()
240
+ const location = useLocation()
241
+
242
+ useEffect(() => {
243
+ // Listen for browser back/forward navigation
244
+ const handlePopState = async () => {
245
+ const flowService = context.service('flow')
246
+ const searchParams = new URLSearchParams(location.search)
247
+ const flowState = searchParams.get('flow')
248
+
249
+ if (flowState) {
250
+ await flowService.load(flowState)
251
+ }
252
+ }
253
+
254
+ window.addEventListener('popstate', handlePopState)
255
+ return () => window.removeEventListener('popstate', handlePopState)
256
+ }, [location])
257
+
258
+ const proceedToStep = async (stepName: string) => {
259
+ const flowService = context.service('flow')
260
+ await flowService.proceed({ target: stepName })
261
+
262
+ // Navigation is handled automatically by the web flow service
263
+ }
264
+
265
+ return (
266
+ <div>
267
+ <button onClick={() => proceedToStep('step1')}>Go to Step 1</button>
268
+ <button onClick={() => proceedToStep('step2')}>Go to Step 2</button>
269
+ <button onClick={() => navigate(-1)}>Browser Back</button>
270
+ </div>
271
+ )
272
+ }
273
+ ```
274
+
275
+ ### Custom Query Parameter Configuration
276
+
277
+ ```typescript
278
+ const context = makeClientContext({
279
+ service: 'custom-app',
280
+ flowConfig: {
281
+ defaultFlow: 'checkout',
282
+ queryParam: 'checkout-state', // Custom parameter name
283
+ services: {
284
+ payment: 'payment-service',
285
+ shipping: 'shipping-service'
286
+ }
287
+ }
288
+ })
289
+
290
+ appendWebFlowService(context)
291
+
292
+ // URLs will look like: /checkout?checkout-state=serialized-flow-data
293
+ ```
294
+
295
+ ### Flow State Persistence
296
+
297
+ ```typescript
298
+ class WebFlowManager {
299
+ constructor(private context: ClientContext) {}
300
+
301
+ async saveFlowToURL(): Promise<string> {
302
+ const flowService = this.context.service('flow')
303
+ const flow = await flowService.state()
304
+
305
+ if (flow) {
306
+ const url = new URL(window.location.href)
307
+ url.searchParams.set('flow', flow.serialize())
308
+
309
+ // Update browser URL without navigation
310
+ window.history.replaceState({}, '', url.toString())
311
+
312
+ return url.toString()
313
+ }
314
+
315
+ return window.location.href
316
+ }
317
+
318
+ async loadFlowFromURL(): Promise<boolean> {
319
+ const searchParams = new URLSearchParams(window.location.search)
320
+ const flowState = searchParams.get('flow')
321
+
322
+ if (flowState) {
323
+ const flowService = this.context.service('flow')
324
+ await flowService.load(flowState)
325
+ return true
326
+ }
327
+
328
+ return false
329
+ }
330
+
331
+ clearFlowFromURL(): void {
332
+ const url = new URL(window.location.href)
333
+ url.searchParams.delete('flow')
334
+ window.history.replaceState({}, '', url.toString())
335
+ }
336
+ }
337
+
338
+ // Usage
339
+ const flowManager = new WebFlowManager(context)
340
+
341
+ // Save current flow state to URL
342
+ await flowManager.saveFlowToURL()
343
+
344
+ // Restore flow from URL on page load
345
+ const restored = await flowManager.loadFlowFromURL()
346
+ ```
347
+
348
+ ## Integration with React Router
349
+
350
+ ```typescript
351
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
352
+ import { appendWebFlowService } from '@owlmeans/web-flow'
353
+
354
+ function App() {
355
+ const context = makeClientContext(config)
356
+ appendWebFlowService(context)
357
+
358
+ return (
359
+ <BrowserRouter>
360
+ <Routes>
361
+ <Route path="/flow/*" element={<FlowRoutes />} />
362
+ <Route path="/dashboard" element={<Dashboard />} />
363
+ </Routes>
364
+ </BrowserRouter>
365
+ )
366
+ }
367
+
368
+ function FlowRoutes() {
369
+ const context = useContext()
370
+ const [searchParams] = useSearchParams()
371
+
372
+ // Flow state is automatically managed through URL
373
+ const flowState = searchParams.get('flow')
374
+
375
+ return (
376
+ <div>
377
+ {flowState ? (
378
+ <ActiveFlowComponent />
379
+ ) : (
380
+ <StartFlowComponent />
381
+ )}
382
+ </div>
383
+ )
384
+ }
385
+ ```
386
+
387
+ ## Best Practices
388
+
389
+ 1. **URL Management**: Use meaningful query parameter names for flow state
390
+ 2. **Browser Integration**: Respect browser navigation patterns
391
+ 3. **State Serialization**: Keep flow state minimal for URL storage
392
+ 4. **Error Handling**: Handle URL parsing errors gracefully
393
+ 5. **SEO Considerations**: Use appropriate meta tags for flow pages
394
+ 6. **Performance**: Optimize flow state serialization for large flows
395
+ 7. **User Experience**: Provide clear navigation indicators
396
+
397
+ ## Dependencies
398
+
399
+ This package depends on:
400
+ - `@owlmeans/client-flow` - Base client flow implementation
401
+ - `@owlmeans/client` - React client library
402
+ - `@owlmeans/flow` - Core flow system
403
+ - `react` - React library (peer dependency)
404
+ - `react-router-dom` - React Router DOM (peer dependency)
405
+
406
+ ## Related Packages
407
+
408
+ - [`@owlmeans/client-flow`](../client-flow) - Base client flow implementation
409
+ - [`@owlmeans/flow`](../flow) - Core flow system
410
+ - [`@owlmeans/server-flow`](../server-flow) - Server-side flow processing
411
+ - [`@owlmeans/client`](../client) - React client library
package/build/.gitkeep ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ export declare const QUERY_PARAM = "flow";
2
+ export declare const SERVICE_PARAM = "service";
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,SAAS,CAAA;AAEjC,eAAO,MAAM,aAAa,YAAY,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const QUERY_PARAM = 'flow';
2
+ export const SERVICE_PARAM = 'service';
3
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAA;AAEjC,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { FlowClient } from '@owlmeans/client-flow';
2
+ export declare const useFlow: (target?: string | null) => FlowClient | null;
3
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,uBAAuB,CAAA;AAKpE,eAAO,MAAM,OAAO,YAAY,MAAM,GAAG,IAAI,KAAU,UAAU,GAAG,IAmBnE,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { useContext, useModule, useNavigate, useValue } from '@owlmeans/client';
2
+ import { createFlowClient } from '@owlmeans/client-flow';
3
+ import { DEFAULT_ALIAS as FLOW_ALIAS } from '@owlmeans/client-flow';
4
+ import { useSearchParams } from 'react-router-dom';
5
+ import { QUERY_PARAM, SERVICE_PARAM } from './consts.js';
6
+ export const useFlow = (target = null) => {
7
+ const context = useContext();
8
+ const nav = useNavigate();
9
+ const [query] = useSearchParams();
10
+ const { params } = useModule();
11
+ const client = useValue(async () => {
12
+ if (QUERY_PARAM in params && params[QUERY_PARAM] != null) {
13
+ const service = context.service(FLOW_ALIAS);
14
+ await service.ready();
15
+ return createFlowClient(context, nav).setup(await service.load(params[QUERY_PARAM]));
16
+ }
17
+ return createFlowClient(context, nav).boot(query.get(SERVICE_PARAM) ?? target);
18
+ }, [
19
+ QUERY_PARAM in params && params[QUERY_PARAM],
20
+ query.get(QUERY_PARAM), query.get(SERVICE_PARAM) ?? target
21
+ ]);
22
+ return client;
23
+ };
24
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EAAE,aAAa,IAAI,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAExD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAwB,IAAI,EAAqB,EAAE;IACzE,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;IACzB,MAAM,CAAC,KAAK,CAAC,GAAG,eAAe,EAAE,CAAA;IACjC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAA;IAC9B,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE;QACjC,IAAI,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC;YACzD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAc,UAAU,CAAC,CAAA;YACxD,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;YAErB,OAAO,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAW,CAAC,CAAC,CAAA;QAChG,CAAC;QACD,OAAO,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC,CAAA;IAChF,CAAC,EAAE;QACD,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,WAAW,CAAW;QACtD,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,MAAM;KAC3D,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
@@ -0,0 +1,5 @@
1
+ export type * from './types.js';
2
+ export * from './service.js';
3
+ export * from './consts.js';
4
+ export * from './helper.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './service.js';
2
+ export * from './consts.js';
3
+ export * from './helper.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { ClientContext, ClientConfig } from '@owlmeans/client-context';
2
+ import type { FlowService } from './types.js';
3
+ export declare const makeFlowService: (alias?: string) => FlowService;
4
+ export declare const appendFlowService: <C extends ClientConfig, T extends ClientContext<C>>(ctx: T, alias?: string) => T;
5
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAQ7C,eAAO,MAAM,eAAe,WAAW,MAAM,KAAmB,WA4E/D,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,OAC7C,CAAC,UAAS,MAAM,KAAmB,CAQzC,CAAA"}
@@ -0,0 +1,78 @@
1
+ import { DEFAULT_ALIAS, makeBasicFlowService, FLOW_STATE } from '@owlmeans/client-flow';
2
+ import { QUERY_PARAM } from './consts.js';
3
+ import { FlowStepMissconfigured, makeFlowModel, UnknownTransition } from '@owlmeans/flow';
4
+ import { ResilientError } from '@owlmeans/error';
5
+ import { assertContext } from '@owlmeans/context';
6
+ import { appendClientResource } from '@owlmeans/client-resource';
7
+ export const makeFlowService = (alias = DEFAULT_ALIAS) => {
8
+ const location = `web-flow-service:${alias}`;
9
+ const service = makeBasicFlowService(alias);
10
+ // @TODO Use in the client proceed also (unify the code)
11
+ service.proceed = async (req, dryRun = false) => {
12
+ const ctx = assertContext(service.ctx, location);
13
+ const flow = service.flow;
14
+ if (flow == null) {
15
+ throw new UnknownTransition('service.proceed');
16
+ }
17
+ const step = flow.step();
18
+ if (step.module == null) {
19
+ throw new FlowStepMissconfigured(step.step);
20
+ }
21
+ const cfg = service.config();
22
+ const param = cfg.queryParam ?? QUERY_PARAM;
23
+ const module = ctx.module(step.module);
24
+ const [url] = await module.call({
25
+ ...req,
26
+ params: { ...req?.params, [param]: flow.serialize() },
27
+ full: true
28
+ });
29
+ // const params = new URLSearchParams(req?.query ?? {})
30
+ // params.set(param, flow.serialize())
31
+ const redirectUrl = new URL(url);
32
+ redirectUrl.searchParams.set(param, flow.serialize());
33
+ if (!dryRun) {
34
+ document.location.href = redirectUrl.toString();
35
+ }
36
+ return redirectUrl.toString();
37
+ };
38
+ service.goHome = async (alias, dryRun = false) => {
39
+ const ctx = assertContext(service.ctx, location);
40
+ const cfg = await ctx.config;
41
+ const targetAlias = alias ?? Object.values(cfg.services).find(s => s.default)?.service ?? cfg.service;
42
+ const target = ctx.serviceRoute(targetAlias);
43
+ const url = target.home ?? cfg.brand?.home ?? 'https://owlmeans.com';
44
+ if (!dryRun) {
45
+ document.location.href = url;
46
+ }
47
+ return url;
48
+ };
49
+ const init = service.lazyInit;
50
+ service.lazyInit = async () => {
51
+ await init();
52
+ const cfg = service.config();
53
+ const param = cfg.queryParam ?? QUERY_PARAM;
54
+ const url = new URL(window.location.href);
55
+ const state = url.searchParams.get(param);
56
+ if (state == null) {
57
+ service.flow = null;
58
+ service.resolvePair().resolve(false);
59
+ return;
60
+ }
61
+ try {
62
+ service.flow = await makeFlowModel(state, service.provideFlow);
63
+ service.resolvePair().resolve(true);
64
+ }
65
+ catch (e) {
66
+ service.flow = null;
67
+ service.resolvePair().reject(ResilientError.ensure(e));
68
+ }
69
+ };
70
+ return service;
71
+ };
72
+ export const appendFlowService = (ctx, alias = DEFAULT_ALIAS) => {
73
+ const service = makeFlowService(alias);
74
+ ctx.registerService(service);
75
+ appendClientResource(ctx, FLOW_STATE);
76
+ return ctx;
77
+ };
78
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { FlowService as ClientFlowService } from '@owlmeans/client-flow';
2
+ export interface FlowService extends ClientFlowService {
3
+ goHome: (alias?: string, dryRun?: boolean) => Promise<string>;
4
+ }
5
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAExE,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC9D"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@owlmeans/web-flow",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "tsc -b",
7
+ "dev": "sleep 366 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
8
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ },
10
+ "main": "build/index.js",
11
+ "module": "build/index.js",
12
+ "types": "build/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./build/index.js",
16
+ "require": "./build/index.js",
17
+ "default": "./build/index.js",
18
+ "module": "./build/index.js",
19
+ "types": "./build/index.d.ts"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@owlmeans/client": "^0.1.0",
24
+ "@owlmeans/client-context": "^0.1.0",
25
+ "@owlmeans/client-flow": "^0.1.0",
26
+ "@owlmeans/client-module": "^0.1.0",
27
+ "@owlmeans/client-resource": "^0.1.0",
28
+ "@owlmeans/context": "^0.1.0",
29
+ "@owlmeans/error": "^0.1.0",
30
+ "@owlmeans/flow": "^0.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "*",
34
+ "react-router-dom": "*"
35
+ },
36
+ "devDependencies": {
37
+ "nodemon": "^3.1.7",
38
+ "npm-check": "^6.0.1",
39
+ "typescript": "^5.6.3"
40
+ },
41
+ "private": false,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export const QUERY_PARAM = 'flow'
3
+
4
+ export const SERVICE_PARAM = 'service'
package/src/helper.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { useContext, useModule, useNavigate, useValue } from '@owlmeans/client'
2
+ import { createFlowClient } from '@owlmeans/client-flow'
3
+ import type { FlowClient, FlowService } from '@owlmeans/client-flow'
4
+ import { DEFAULT_ALIAS as FLOW_ALIAS } from '@owlmeans/client-flow'
5
+ import { useSearchParams } from 'react-router-dom'
6
+ import { QUERY_PARAM, SERVICE_PARAM } from './consts.js'
7
+
8
+ export const useFlow = (target: string | null = null): FlowClient | null => {
9
+ const context = useContext()
10
+ const nav = useNavigate()
11
+ const [query] = useSearchParams()
12
+ const { params } = useModule()
13
+ const client = useValue(async () => {
14
+ if (QUERY_PARAM in params && params[QUERY_PARAM] != null) {
15
+ const service = context.service<FlowService>(FLOW_ALIAS)
16
+ await service.ready()
17
+
18
+ return createFlowClient(context, nav).setup(await service.load(params[QUERY_PARAM] as string))
19
+ }
20
+ return createFlowClient(context, nav).boot(query.get(SERVICE_PARAM) ?? target)
21
+ }, [
22
+ QUERY_PARAM in params && params[QUERY_PARAM] as string,
23
+ query.get(QUERY_PARAM), query.get(SERVICE_PARAM) ?? target
24
+ ])
25
+
26
+ return client
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type * from './types.js'
2
+ export * from './service.js'
3
+ export * from './consts.js'
4
+ export * from './helper.js'
package/src/service.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { ClientContext, ClientConfig } from '@owlmeans/client-context'
2
+ import { DEFAULT_ALIAS, makeBasicFlowService, FLOW_STATE } from '@owlmeans/client-flow'
3
+ import type { FlowService } from './types.js'
4
+ import { QUERY_PARAM } from './consts.js'
5
+ import { FlowStepMissconfigured, makeFlowModel, UnknownTransition } from '@owlmeans/flow'
6
+ import { ResilientError } from '@owlmeans/error'
7
+ import { assertContext } from '@owlmeans/context'
8
+ import type { ClientModule } from '@owlmeans/client-module'
9
+ import { appendClientResource } from '@owlmeans/client-resource'
10
+
11
+ export const makeFlowService = (alias: string = DEFAULT_ALIAS): FlowService => {
12
+ const location = `web-flow-service:${alias}`
13
+ const service: FlowService = makeBasicFlowService(alias) as FlowService
14
+
15
+ // @TODO Use in the client proceed also (unify the code)
16
+ service.proceed = async (req, dryRun = false) => {
17
+ const ctx = assertContext(service.ctx, location) as ClientContext
18
+ const flow = service.flow
19
+ if (flow == null) {
20
+ throw new UnknownTransition('service.proceed')
21
+ }
22
+ const step = flow.step()
23
+ if (step.module == null) {
24
+ throw new FlowStepMissconfigured(step.step)
25
+ }
26
+
27
+ const cfg = service.config()
28
+ const param = cfg.queryParam ?? QUERY_PARAM
29
+
30
+ const module = ctx.module<ClientModule<string>>(step.module)
31
+ const [url] = await module.call<string>({
32
+ ...req,
33
+ params: { ...req?.params, [param]: flow.serialize() },
34
+ full: true
35
+ })
36
+
37
+ // const params = new URLSearchParams(req?.query ?? {})
38
+ // params.set(param, flow.serialize())
39
+
40
+ const redirectUrl = new URL(url)
41
+ redirectUrl.searchParams.set(param, flow.serialize())
42
+
43
+ if (!dryRun) {
44
+ document.location.href = redirectUrl.toString()
45
+ }
46
+
47
+ return redirectUrl.toString()
48
+ }
49
+
50
+ service.goHome = async (alias, dryRun = false) => {
51
+ const ctx = assertContext(service.ctx, location) as ClientContext
52
+ const cfg = await ctx.config
53
+ const targetAlias = alias ?? Object.values(cfg.services).find(s => s.default)?.service ?? cfg.service
54
+ const target = ctx.serviceRoute(targetAlias)
55
+
56
+ const url = target.home ?? cfg.brand?.home ?? 'https://owlmeans.com'
57
+ if (!dryRun) {
58
+ document.location.href = url
59
+ }
60
+
61
+ return url
62
+ }
63
+
64
+ const init = service.lazyInit
65
+ service.lazyInit = async () => {
66
+ await init()
67
+ const cfg = service.config()
68
+ const param = cfg.queryParam ?? QUERY_PARAM
69
+ const url = new URL(window.location.href)
70
+ const state = url.searchParams.get(param)
71
+ if (state == null) {
72
+ service.flow = null
73
+ service.resolvePair().resolve(false)
74
+ return
75
+ }
76
+
77
+ try {
78
+ service.flow = await makeFlowModel(state, service.provideFlow)
79
+ service.resolvePair().resolve(true)
80
+ } catch (e) {
81
+ service.flow = null
82
+ service.resolvePair().reject(ResilientError.ensure(e as Error))
83
+ }
84
+ }
85
+
86
+ return service
87
+ }
88
+
89
+ export const appendFlowService = <
90
+ C extends ClientConfig, T extends ClientContext<C>
91
+ >(ctx: T, alias: string = DEFAULT_ALIAS): T => {
92
+ const service = makeFlowService(alias)
93
+
94
+ ctx.registerService(service)
95
+
96
+ appendClientResource<C, T>(ctx, FLOW_STATE)
97
+
98
+ return ctx
99
+ }
package/src/types.ts ADDED
@@ -0,0 +1,6 @@
1
+
2
+ import { FlowService as ClientFlowService } from '@owlmeans/client-flow'
3
+
4
+ export interface FlowService extends ClientFlowService {
5
+ goHome: (alias?: string, dryRun?: boolean) => Promise<string>
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": [
3
+ "../tsconfig.default.json",
4
+ "../tsconfig.react.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "rootDir": "./src/", /* Specify the root folder within your source files. */
8
+ "outDir": "./build/", /* Specify an output folder for all emitted files. */
9
+ "moduleResolution": "Bundler"
10
+ },
11
+ "exclude": [
12
+ "./dist/**/*",
13
+ "./build/**/*",
14
+ "./*.ts"
15
+ ]
16
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/consts.ts","./src/helper.ts","./src/index.ts","./src/service.ts","./src/types.ts"],"version":"5.6.3"}