feat: Add lifecycle hooks support to onboarding orchestrator

- Add onFlowWillStart, onFlowDidStart, onStepWillChange, onStepDidChange, onFlowWillEnd, onFlowDidEnd hooks
- Hooks are optional and backward compatible with existing flows
- Add getCurrentPath() and isStepInPath() helper methods for multi-path flows
- Support dynamic next() functions (already existed, now documented)
- Maintain full backward compatibility with default@v1 flow
This commit is contained in:
roncodes
2025-12-11 23:10:23 -05:00
parent a29ca0ecb9
commit a9b172081a

View File

@@ -12,14 +12,26 @@ export default class OnboardingOrchestratorService extends Service {
@tracked history = [];
@tracked sessionId = null;
start(flowId = null, opts = {}) {
async start(flowId = null, opts = {}) {
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
this.flow = flow;
this.wrapper = flow.wrapper || null;
this.sessionId = opts.sessionId || null;
this.history = [];
this.goto(flow.entry);
// Execute onFlowWillStart hook if defined
if (typeof this.flow.onFlowWillStart === 'function') {
await this.flow.onFlowWillStart(this.flow, this);
}
await this.goto(flow.entry);
// Execute onFlowDidStart hook if defined
if (typeof this.flow.onFlowDidStart === 'function') {
await this.flow.onFlowDidStart(this.flow, this);
}
}
async goto(stepId) {
@@ -27,27 +39,43 @@ export default class OnboardingOrchestratorService extends Service {
const step = this.flow.steps.find((s) => s.id === stepId);
if (!step) throw new Error(`Step '${stepId}' not found`);
// Execute onStepWillChange hook if defined
const previousStep = this.current;
if (typeof this.flow.onStepWillChange === 'function') {
await this.flow.onStepWillChange(step, previousStep, this);
}
// Guard function - skip step if guard returns false
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
return this.next();
}
// beforeEnter lifecycle hook
if (typeof step.beforeEnter === 'function') {
await step.beforeEnter(this.onboardingContext);
}
this.current = step;
// Execute onStepDidChange hook if defined
if (typeof this.flow.onStepDidChange === 'function') {
await this.flow.onStepDidChange(this.current, previousStep, this);
}
}
async next() {
if (!this.flow || !this.current) return;
const leaving = this.current;
// afterLeave lifecycle hook
if (typeof leaving.afterLeave === 'function') {
await leaving.afterLeave(this.onboardingContext);
}
if (!this.history.includes(leaving)) this.history.push(leaving);
// Support both string and function for next property
let nextId;
if (typeof leaving.next === 'function') {
nextId = leaving.next(this.onboardingContext);
@@ -55,8 +83,20 @@ export default class OnboardingOrchestratorService extends Service {
nextId = leaving.next;
}
// If no next step, flow is complete
if (!nextId) {
// Execute onFlowWillEnd hook if defined
if (typeof this.flow.onFlowWillEnd === 'function') {
await this.flow.onFlowWillEnd(leaving, this);
}
this.current = null; // finished
// Execute onFlowDidEnd hook if defined
if (typeof this.flow.onFlowDidEnd === 'function') {
await this.flow.onFlowDidEnd(leaving, this);
}
return;
}
@@ -70,4 +110,31 @@ export default class OnboardingOrchestratorService extends Service {
this.history = this.history.slice(0, -1);
await this.goto(prev.id);
}
/**
* Get the current path (for flows with multiple paths)
* This is a helper method that can be used by flows to determine the current path
*/
getCurrentPath() {
if (!this.flow || !this.flow.paths) return null;
// Determine path based on context or current step
for (const [pathId, pathDef] of Object.entries(this.flow.paths)) {
if (pathDef.steps && pathDef.steps.some(s => s.id === this.current?.id)) {
return pathDef;
}
}
return null;
}
/**
* Check if a step is in the current path
*/
isStepInPath(stepId) {
const currentPath = this.getCurrentPath();
if (!currentPath) return true; // If no paths defined, all steps are valid
return currentPath.steps?.some(s => s.id === stepId) ?? false;
}
}