PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / state / tours.js

tours.js in Extendify 3.2.1, at src/Agent/state/tours.js

221 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { safeParseJson } from '@shared/lib/parsing';
2 import apiFetch from '@wordpress/api-fetch';
3 import { create } from 'zustand';
4 import { createJSONStorage, devtools, persist } from 'zustand/middleware';
5
6 // keep help-center here to maintain compatibility with the old help center code
7 const key = 'extendify-help-center-tour-progress';
8 const startingState = {
9 currentTour: null,
10 currentStep: undefined,
11 preparingStep: undefined,
12 progress: [],
13 // initialize the state with default values
14 ...(safeParseJson(window.extAgentData.userData.tourData)?.state ?? {}),
15 };
16
17 const state = (set, get) => ({
18 ...startingState,
19 startTour: async (tourData) => {
20 const { trackTourProgress, updateProgress, getStepData, onTourPage } =
21 get();
22
23 if (onTourPage(tourData?.settings?.startFrom)) {
24 window.dispatchEvent(new CustomEvent('extendify-agent:close'));
25 await tourData?.onStart?.(tourData);
26 tourData.steps =
27 tourData.steps?.filter(
28 // Filter out steps that define a condition
29 (s) => s?.showOnlyIf?.() || s?.showOnlyIf?.() === undefined,
30 ) || [];
31 await getStepData(0, tourData)?.events?.beforeAttach?.(tourData);
32 }
33
34 set({ currentTour: tourData, currentStep: 0, preparingStep: undefined });
35 // Increment the opened count
36 const tour = trackTourProgress(tourData.id);
37 updateProgress(tour.id, {
38 openedCount: Number(tour.openedCount) + 1,
39 lastAction: 'started',
40 });
41 },
42 onTourPage: (startFrom = null) => {
43 const url = window.location.href;
44 if (startFrom?.includes(url)) return true;
45 const { currentTour } = get();
46 return currentTour?.settings?.startFrom?.includes(url);
47 },
48 completeCurrentTour: async () => {
49 const { currentTour, wasCompleted, findTourProgress, updateProgress } =
50 get();
51 const tour = findTourProgress(currentTour?.id);
52 if (!tour?.id) return;
53 // if already completed, don't update the completedAt
54 if (!wasCompleted(tour.id)) {
55 updateProgress(tour.id, {
56 completedAt: new Date().toISOString(),
57 lastAction: 'completed',
58 });
59 }
60 // Track how many times it was completed
61 updateProgress(tour.id, {
62 completedCount: Number(tour.completedCount) + 1,
63 lastAction: 'completed',
64 });
65 await currentTour?.onDetach?.();
66 await currentTour?.onFinish?.();
67 set({ currentTour: null, currentStep: undefined });
68 window.dispatchEvent(new CustomEvent('extendify-agent:open'));
69 },
70 closeCurrentTour: async (lastAction) => {
71 const { currentTour, findTourProgress, updateProgress } = get();
72 const tour = findTourProgress(currentTour?.id);
73 if (!tour?.id) return;
74 const additional = {};
75 if (['redirected'].includes(lastAction)) {
76 return updateProgress(tour?.id, { lastAction });
77 }
78 if (['closed-by-caught-error'].includes(lastAction)) {
79 return updateProgress(tour?.id, { lastAction, errored: true });
80 }
81 if (lastAction === 'closed-manually') {
82 additional.closedManuallyCount = Number(tour.closedManuallyCount) + 1;
83 }
84
85 await currentTour?.onDetach?.();
86 await currentTour?.onFinish?.();
87 updateProgress(tour?.id, { lastAction, ...additional });
88 set({
89 currentTour: null,
90 currentStep: undefined,
91 preparingStep: undefined,
92 });
93 window.dispatchEvent(new CustomEvent('extendify-agent:open'));
94 },
95 findTourProgress: (tourId) =>
96 get().progress.find((tour) => tour.id === tourId),
97 wasCompleted: (tourId) => get().findTourProgress(tourId)?.completedAt,
98 wasOpened: (tourId) =>
99 Number(get().findTourProgress(tourId)?.openedCount ?? 0) > 0,
100 isSeen: (tourId) => get().findTourProgress(tourId)?.firstSeenAt,
101 trackTourProgress: (tourId) => {
102 const { findTourProgress } = get();
103 // If we are already tracking it, return that
104 if (findTourProgress(tourId)) {
105 return findTourProgress(tourId);
106 }
107 set((state) => ({
108 progress: [
109 ...state.progress,
110 {
111 id: tourId,
112 firstSeenAt: new Date().toISOString(),
113 updatedAt: new Date().toISOString(),
114 completedAt: null,
115 lastAction: 'init',
116 currentStep: 0,
117 openedCount: 0,
118 closedManuallyCount: 0,
119 completedCount: 0,
120 errored: false,
121 },
122 ],
123 }));
124 return findTourProgress(tourId);
125 },
126 updateProgress: (tourId, update) => {
127 const lastAction = update?.lastAction ?? 'unknown';
128 set((state) => {
129 const progress = state.progress.map((tour) => {
130 if (tour.id === tourId) {
131 return {
132 ...tour,
133 ...update,
134 lastAction,
135 updatedAt: new Date().toISOString(),
136 };
137 }
138 return tour;
139 });
140 return { progress };
141 });
142 },
143 getStepData: (step, tour = get().currentTour) => tour?.steps?.[step] ?? {},
144 hasNextStep: () => {
145 if (!get().currentTour) return false;
146 return Number(get().currentStep) < get().currentTour.steps.length - 1;
147 },
148 nextStep: async () => {
149 const { currentTour, goToStep, updateProgress, currentStep } = get();
150 const step = Number(currentStep) + 1;
151 await goToStep(step);
152 updateProgress(currentTour.id, {
153 currentStep: step,
154 lastAction: 'next',
155 });
156 },
157 hasPreviousStep: () => {
158 if (!get().currentTour) return false;
159 return Number(get().currentStep) > 0;
160 },
161 prevStep: async () => {
162 const { currentTour, goToStep, updateProgress, currentStep } = get();
163 const step = currentStep - 1;
164 await goToStep(step);
165 updateProgress(currentTour.id, {
166 currentStep: step,
167 lastAction: 'prev',
168 });
169 },
170 goToStep: async (step) => {
171 const { currentTour, updateProgress, closeCurrentTour, getStepData } =
172 get();
173 const tour = currentTour;
174
175 // Check that the step is valid
176 if (step < 0 || step > tour.steps.length - 1) {
177 closeCurrentTour('closed-by-caught-error');
178 return;
179 }
180
181 updateProgress(tour.id, {
182 currentStep: step,
183 lastAction: `go-to-step-${step}`,
184 });
185
186 const events = getStepData(step)?.events;
187
188 if (events?.beforeAttach) {
189 set(() => ({ preparingStep: step }));
190 // Make sure the preparing animation runs at least 300ms
191 await Promise.allSettled([
192 events.beforeAttach?.(tour),
193 new Promise((resolve) => setTimeout(resolve, 300)),
194 ]);
195 set(() => ({ preparingStep: undefined }));
196 }
197
198 set(() => ({ currentStep: step }));
199 },
200 });
201
202 const path = '/extendify/v1/help-center/tour-data';
203 const storage = {
204 getItem: async () => await apiFetch({ path }),
205 setItem: async (_name, state) =>
206 await apiFetch({ path, method: 'POST', data: { state } }),
207 };
208
209 export const useTourStore = create(
210 persist(devtools(state, { name: 'Extendify Tour Progress' }), {
211 name: key,
212 storage: createJSONStorage(() => storage),
213 skipHydration: true,
214 partialize: (state) => {
215 // return without currentTour or currentStep
216 const { currentTour, currentStep, preparingStep, ...newState } = state;
217 return newState;
218 },
219 }),
220 );
221