Skip to content

Commit 9bdfcb3

Browse files
authored
Improved event handler api (#411)
* feat: new and improved event handling api * add unit tests * remaining fixes * fix switch
1 parent 3d7d73d commit 9bdfcb3

File tree

9 files changed

+407
-6
lines changed

9 files changed

+407
-6
lines changed

.changeset/proud-pets-fly.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@frigade/js": minor
3+
"@frigade/react": minor
4+
---
5+
6+
Adds a new and improved API for handling state changes which allows you to filter events based on flow and step completion for the user. The API is available through `frigade.on(...)`

apps/smithy/src/stories/Announcement/Announcement.stories.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Announcement, Tour, useFlow, useFrigade } from "@frigade/react";
2+
import { useEffect } from "react";
23

34
export default {
45
title: "Components/Announcement",
@@ -24,6 +25,25 @@ export const TestReset = {
2425
const { frigade } = useFrigade();
2526
const { flow } = useFlow(args.flowId);
2627

28+
useEffect(() => {
29+
frigade.on("step.start", (event, flow, previousFlow, step) => {
30+
console.log("step.start", event, flow.id, step?.id);
31+
});
32+
frigade.on("step.complete", (event, flow, previousFlow, step) => {
33+
console.log("step.complete", event, flow.id, step?.id);
34+
});
35+
36+
frigade.on("flow.start", (event, flow) => {
37+
console.log("flow.start", event, flow.id);
38+
});
39+
frigade.on("flow.complete", (event, flow) => {
40+
console.log("flow.complete", event, flow.id);
41+
});
42+
frigade.on("flow.skip", (event, flow) => {
43+
console.log("flow.skip", event, flow.id);
44+
});
45+
}, []);
46+
2747
return (
2848
<div>
2949
<Story {...args} />

packages/js-api/src/core/flow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ export class Flow extends Fetchable {
498498

499499
/**
500500
* @ignore
501+
* @deprecated Use `frigade.on('flow.complete' | 'flow.skip' | 'flow.restart' | 'flow.start', handler)` instead.
501502
*/
502503
public onStateChange(handler: (flow: Flow, previousFlow: Flow) => void) {
503504
const wrapperHandler = (flow: Flow, previousFlow: Flow) => {

packages/js-api/src/core/frigade.ts

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {
2+
FlowChangeEvent,
3+
FlowChangeEventHandler,
24
FlowStateDTO,
35
FlowStates,
6+
FlowStep,
47
FrigadeConfig,
58
PropertyPayload,
69
SessionDTO,
@@ -42,6 +45,10 @@ export class Frigade extends Fetchable {
4245
* @ignore
4346
*/
4447
private lastSessionDTO?: SessionDTO
48+
/**
49+
* @ignore
50+
*/
51+
private eventHandlers: Map<FlowChangeEvent, FlowChangeEventHandler[]> = new Map()
4552

4653
/**
4754
* @ignore
@@ -366,10 +373,14 @@ export class Frigade extends Fetchable {
366373
}
367374
this.initPromise = null
368375
await this.init(this.config)
376+
this.triggerAllLegacyEventHandlers()
369377
this.triggerAllEventHandlers()
370378
}
371379

372-
private triggerAllEventHandlers() {
380+
/**
381+
* @ignore
382+
*/
383+
private triggerAllLegacyEventHandlers() {
373384
this.flows.forEach((flow) => {
374385
this.getGlobalState().onFlowStateChangeHandlers.forEach((handler) => {
375386
const lastFlow = this.getGlobalState().previousFlows.get(flow.id)
@@ -379,19 +390,48 @@ export class Frigade extends Fetchable {
379390
})
380391
}
381392

393+
private triggerAllEventHandlers() {
394+
this.flows.forEach((flow) => {
395+
const lastFlow = this.getGlobalState().previousFlows.get(flow.id)
396+
this.triggerEventHandlers(flow.rawData, lastFlow?.rawData)
397+
})
398+
}
399+
382400
private async resync() {
383401
await this.refreshStateFromAPI()
384402
}
385403

386404
/**
387405
* Event handler that captures all changes that happen to the state of the Flows.
406+
* @deprecated Use `frigade.on` instead.
388407
* @param handler
389408
*/
390409
public async onStateChange(handler: (flow: Flow, previousFlow?: Flow) => void) {
391410
await this.initIfNeeded()
392411
this.getGlobalState().onFlowStateChangeHandlers.push(handler)
393412
}
394413

414+
/**
415+
* Event handler that captures all changes that happen to the state of the Flows. Use `flow.any` to capture all events.
416+
* @param event `flow.any` | `flow.complete` | `flow.restart` | `flow.skip` | `flow.start` | `step.complete` | `step.skip` | `step.reset` | `step.start`
417+
* @param handler
418+
*/
419+
public async on(event: FlowChangeEvent, handler: FlowChangeEventHandler) {
420+
this.eventHandlers.set(event, [...(this.eventHandlers.get(event) ?? []), handler])
421+
}
422+
423+
/**
424+
* Removes the given handler.
425+
* @param event `flow.any` | `flow.complete` | `flow.restart` | `flow.skip` | `flow.start` | `step.complete` | `step.skip` | `step.reset` | `step.start`
426+
* @param handler
427+
*/
428+
public async off(event: FlowChangeEvent, handler: FlowChangeEventHandler) {
429+
this.eventHandlers.set(
430+
event,
431+
(this.eventHandlers.get(event) ?? []).filter((h) => h !== handler)
432+
)
433+
}
434+
395435
/**
396436
* Returns true if the JS SDK failed to connect to the Frigade API.
397437
*/
@@ -435,6 +475,7 @@ export class Frigade extends Fetchable {
435475
const previousState = target[key] as StatefulFlow
436476
const newState = value as StatefulFlow
437477
if (JSON.stringify(previousState) !== JSON.stringify(newState)) {
478+
that.triggerDeprecatedEventHandlers(newState, previousState)
438479
that.triggerEventHandlers(newState, previousState)
439480
}
440481
}
@@ -515,6 +556,7 @@ export class Frigade extends Fetchable {
515556
// Necessary check to prevent race condition between flow state and collection state
516557
!overrideFlowStateRaw
517558
) {
559+
this.triggerAllLegacyEventHandlers()
518560
this.triggerAllEventHandlers()
519561
}
520562
}
@@ -588,7 +630,10 @@ export class Frigade extends Fetchable {
588630
/**
589631
* @ignore
590632
*/
591-
private async triggerEventHandlers(newState: StatefulFlow, previousState?: StatefulFlow) {
633+
private async triggerDeprecatedEventHandlers(
634+
newState: StatefulFlow,
635+
previousState?: StatefulFlow
636+
) {
592637
if (newState) {
593638
this.flows.forEach((flow) => {
594639
if (flow.id == previousState.flowSlug) {
@@ -603,6 +648,113 @@ export class Frigade extends Fetchable {
603648
}
604649
}
605650

651+
/**
652+
* @ignore
653+
*/
654+
private triggerEventHandlers(newState: StatefulFlow, previousState?: StatefulFlow) {
655+
if (newState) {
656+
for (const flow of this.flows) {
657+
if (flow.id == newState.flowSlug) {
658+
const lastFlow = this.getGlobalState().previousFlows.get(flow.id)
659+
flow.resyncState(newState)
660+
for (const [event, handlers] of this.eventHandlers.entries()) {
661+
switch (event) {
662+
case 'flow.complete':
663+
if (newState.$state.completed && previousState?.$state.completed === false) {
664+
handlers.forEach((handler) => handler(event, flow, lastFlow))
665+
}
666+
break
667+
case 'flow.restart':
668+
if (!newState.$state.started && previousState?.$state.started === true) {
669+
handlers.forEach((handler) => handler(event, flow, lastFlow))
670+
}
671+
break
672+
case 'flow.skip':
673+
if (newState.$state.skipped && previousState?.$state.skipped === false) {
674+
handlers.forEach((handler) => handler(event, flow, lastFlow))
675+
}
676+
break
677+
case 'flow.start':
678+
if (newState.$state.started && previousState?.$state.started === false) {
679+
handlers.forEach((handler) => handler(event, flow, lastFlow))
680+
}
681+
break
682+
case 'step.complete':
683+
for (const step of newState.data.steps ?? []) {
684+
if (
685+
step.$state.completed &&
686+
!previousState?.data.steps.find(
687+
(previousStepState) =>
688+
previousStepState.id === step.id && previousStepState.$state.completed
689+
)
690+
) {
691+
handlers.forEach((handler) =>
692+
handler(event, flow, lastFlow, flow.steps.get(step.id))
693+
)
694+
}
695+
}
696+
break
697+
case 'step.reset':
698+
for (const step of newState.data.steps ?? []) {
699+
const previousStep = previousState?.data.steps.find(
700+
(previousStepState) => previousStepState.id === step.id
701+
)
702+
if (
703+
step.$state.started == false &&
704+
!step.$state.lastActionAt &&
705+
previousStep?.$state.started &&
706+
previousStep?.$state.lastActionAt
707+
) {
708+
handlers.forEach((handler) =>
709+
handler(event, flow, lastFlow, flow.steps.get(step.id))
710+
)
711+
}
712+
}
713+
break
714+
case 'step.skip':
715+
for (const step of newState.data.steps ?? []) {
716+
if (
717+
step.$state.skipped &&
718+
!previousState?.data.steps.find(
719+
(previousStepState) =>
720+
previousStepState.id === step.id && previousStepState.$state.skipped
721+
)
722+
) {
723+
handlers.forEach((handler) =>
724+
handler(event, flow, lastFlow, flow.steps.get(step.id))
725+
)
726+
}
727+
}
728+
break
729+
case 'step.start':
730+
for (const step of newState.data.steps ?? []) {
731+
if (
732+
step.$state.started &&
733+
previousState?.data.steps.find(
734+
(previousStepState) =>
735+
previousStepState.id === step.id &&
736+
previousStepState.$state.started === false
737+
)
738+
) {
739+
handlers.forEach((handler) =>
740+
handler(event, flow, lastFlow, flow.steps.get(step.id))
741+
)
742+
}
743+
}
744+
break
745+
case 'flow.any':
746+
if (JSON.stringify(newState) !== JSON.stringify(previousState ?? {})) {
747+
handlers.forEach((handler) => handler(event, flow, lastFlow))
748+
}
749+
break
750+
}
751+
}
752+
this.getGlobalState().previousFlows.set(flow.id, cloneFlow(flow))
753+
}
754+
}
755+
}
756+
}
757+
606758
/**
607759
* @ignore
608760
*/

packages/js-api/src/core/types.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,36 @@ export type StepAction =
2424
| 'step.start'
2525
| false
2626

27+
export type FlowChangeEvent =
28+
| 'flow.any'
29+
| 'flow.complete'
30+
| 'flow.restart'
31+
| 'flow.skip'
32+
| 'flow.start'
33+
| 'step.complete'
34+
| 'step.skip'
35+
| 'step.reset'
36+
| 'step.start'
37+
38+
export type FlowChangeEventHandler = (
39+
/**
40+
* The event that triggered the handler.
41+
*/
42+
event: FlowChangeEvent,
43+
/**
44+
* The Flow that triggered the event.
45+
*/
46+
flow: Flow,
47+
/**
48+
* The previous Flow that triggered the event.
49+
*/
50+
previousFlow?: Flow,
51+
/**
52+
* The step that triggered the event. Only applicable for `step.complete`, `step.skip`, `step.reset`, `step.start` events.
53+
*/
54+
step?: FlowStep
55+
) => void
56+
2757
export type PropertyPayload = Record<string, any>
2858

2959
export interface FlowStep extends StatefulStep {
@@ -223,6 +253,7 @@ export interface FlowStep extends StatefulStep {
223253

224254
/**
225255
* Event handler called when this step's state changes.
256+
* @deprecated Use `frigade.on('step.complete' | 'step.skip' | 'step.reset' | 'step.start', handler)` instead.
226257
*/
227258
onStateChange: (callback: (step: FlowStep, previousStep?: FlowStep) => void) => void
228259

packages/js-api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export {
88
type StatefulFlow,
99
TriggerType,
1010
PropertyPayload,
11+
FlowChangeEvent,
12+
FlowChangeEventHandler,
1113
} from './core/types'
1214
export type {
1315
EnrichedCollection,

0 commit comments

Comments
 (0)