PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / trunk
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance vtrunk
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / team-updraft / lib-onboarding-wizard / Wizard / Onboarding / src / components / Onboarding.tsx

Onboarding.tsx in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance trunk, at vendor/team-updraft/lib-onboarding-wizard/Wizard/Onboarding/src/components/Onboarding.tsx

318 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {useEffect, memo, useMemo, useState, useRef} from "@wordpress/element";
2 import Modal from "./Modal/Modal";
3 import { ErrorBoundary } from './ErrorBoundary';
4 import useOnboardingStore from "../store/useOnboardingStore";
5 import { ModalTopBar } from "./Modal/ModalTopBar";
6 import { ModalHeader } from "./Modal/ModalHeader";
7 import { ModalContent } from "./Modal/ModalContent";
8 import { ModalFooter } from "./Modal/ModalFooter";
9 import { __ } from '@wordpress/i18n';
10 import {TooltipContainerProvider} from "../utils/Tooltip/TooltipContainerContext";
11 import { isValidEmail } from '../utils/validators';
12 import Alert from './Alert';
13 import useAlertStore from "../store/useAlertStore";
14 import {updateAction} from "../utils/api";
15
16 /**
17 * Onboarding component that guides users through a series of steps
18 * @returns {JSX.Element} The Onboarding component
19 */
20 const Onboarding = () => {
21 const [ConfettiExplosion, setConfettiExplosion] = useState(null);
22 const [isExploding, setIsExploding] = useState(false);
23
24 const {
25 isOpen,
26 currentStepIndex,
27 steps,
28 setOpen,
29 setCurrentStepIndex,
30 addSuccessStep,
31 setSteps,
32 onboardingData,
33 setResponseMessage,
34 setResponseSuccess,
35 responseMessage,
36 responseSuccess,
37 responseCode,
38 updateEmail,
39 updateStepSettings,
40 installPlugins,
41 validateLicense,
42 licenseStatus,
43 settings,
44 setValue,
45 getSettings,
46 isLastStep,
47 trackingTestRunning,
48 isInstalling,
49 continueDisabled,
50 setContinueDisabled,
51 isUpdating,
52 wizardCompletionPromise,
53 } = useOnboardingStore();
54
55 const { setAlertState } = useAlertStore(); // Get setAlertState from useAlertStore
56
57 useEffect(() => {
58 if ( !!isLastStep() ) {
59 import ( "react-confetti-explosion").then(
60 ({default: ConfettiExplosion}) => {
61 setConfettiExplosion(() => ConfettiExplosion);
62 });
63 setIsExploding(true);
64 }
65 }, [isLastStep()]);
66
67 // Memoize the settings and visible steps to prevent unnecessary recalculations
68 const visibleSteps = useMemo(() => onboardingData.steps?.filter(step => step.visible !== false) || [], [onboardingData.steps]);
69 const currentStep = visibleSteps[currentStepIndex];
70 const tooltipContainerRef = useRef<HTMLDivElement>(null);
71
72 // Initialize steps only once when the component mounts
73 useEffect(() => {
74
75 if (visibleSteps.length > 0 && steps.length === 0) {
76 setSteps(visibleSteps);
77 }
78 }, [visibleSteps, steps.length, setSteps]);
79
80 useEffect(() => {
81 if ( settings.length === 0 ) {
82 getSettings();
83 }
84 }, [settings, getSettings]);
85
86 useEffect(() => {
87 setResponseMessage('');
88 setResponseSuccess(true);
89 }, [currentStepIndex, setResponseMessage, setResponseSuccess]);
90
91 const handleClose = () => {
92 setOpen(false);
93 updateAction({}, 'user_skipped_wizard');
94 };
95
96 const handlePrevious = () => {
97 setCurrentStepIndex(currentStepIndex - 1);
98 setContinueDisabled(false);
99 };
100
101 const validateAndContinue = async (e) => {
102 // Always clear previous response state upon a new submission attempt
103 setResponseMessage('');
104
105 // Do a dynamic external action check at the start of validation. If it exists, use it — this lets specific plugin functions run without adding them to the library.
106 if (currentStep.onContinueExternalAction) {
107 const externalActionName = currentStep.onContinueExternalAction;
108 const externalAction = (window as any).pluginOnboardingActions?.[externalActionName];
109
110 if (typeof externalAction === 'function') {
111 const externalActionResult = await externalAction(
112 currentStep,
113 settings,
114 setAlertState,
115 setValue
116 );
117
118 if (!externalActionResult?.success) {
119 setResponseMessage(externalActionResult?.message ?? __("External action for this step is not available.", "ONBOARDING_WIZARD_TEXT_DOMAIN"));
120 setResponseSuccess(false);
121 return; // Prevent continuing if the external action indicates failure
122 }
123 } else {
124 console.warn(`External action '${externalActionName}' not found in window.pluginOnboardingActions.`);
125 setResponseMessage(__("External action for this step is not available.", "ONBOARDING_WIZARD_TEXT_DOMAIN"));
126 setResponseSuccess(false);
127 return;
128 }
129 }
130
131 let success = true;
132 if ( currentStep.type === 'license' && licenseStatus !== 'activated') {
133 await validateLicense();
134 // Do not auto-advance for license; show the result view with the message
135 return;
136 }
137
138 //save the current values
139 if ( currentStep.type === 'settings' ) {
140 await updateStepSettings(settings);
141 }
142
143 if ( currentStep.type === 'email' ) {
144 await updateEmail();
145 }
146
147 if (currentStep.type === 'plugins'){
148 const allInstalled = onboardingData.is_all_plugins_installed;
149 if(!allInstalled) {
150 //we don't wait for the plugins to be installed, so the user can continue.
151 //Make this call async after the render, e.g., in a useEffect or setTimeout:
152 setTimeout(() => {
153 installPlugins();
154 }, 0);
155 }
156 }
157
158 if ( !success ) {
159 return;
160 }
161 await handleContinue(e);
162 }
163
164 const changeFieldValue = async (fieldId: string, value: any) => {
165 setValue(fieldId, value);
166 };
167
168 // Function to determine if the continue button should be disabled.
169 const isContinueDisabled = () => {
170 if (continueDisabled) {
171 return true;
172 }
173
174 // For tracking test step, only disable continue button while test is running.
175 if (currentStep?.id === 'tracking') {
176 return trackingTestRunning;
177 }
178
179 // we don't want to close the modal during installation. Otherwise it might not complete.
180 if ( isLastStep() && isInstalling ) {
181 return true;
182 }
183
184 // Disable when on an email-related step only if an email field exists, and it's empty or invalid.
185 if (currentStep?.type === 'email' || currentStep?.type === 'license') {
186 const emailField = currentStep.fields?.find((f: any) => f?.type === 'email');
187
188 if (emailField) {
189 const rawValue =
190 (settings && typeof settings === 'object' && !Array.isArray(settings) ? settings[emailField.id] : undefined) ??
191 (Array.isArray(settings) ? settings.find((s: any) => s?.id === emailField.id)?.value : undefined) ??
192 '';
193
194 if (!isValidEmail(rawValue)) {
195 return true;
196 }
197 }
198
199 if (currentStep?.type === 'email') {
200 const checkboxField = currentStep.fields?.find((f: any) => f?.type === 'checkbox');
201 if (checkboxField) {
202 const checkboxValue =
203 (settings && typeof settings === 'object' && !Array.isArray(settings)
204 ? (settings as any)[checkboxField.id]
205 : undefined) ??
206 (Array.isArray(settings)
207 ? (settings as any[]).find((s: any) => s?.id === checkboxField.id)?.value
208 : undefined) ??
209 false;
210
211 if (checkboxValue !== true) {
212 return true;
213 }
214 }
215 }
216 }
217 return false;
218 };
219
220 const handleContinue = async (e) => {
221 // make sure the response message is cleared
222 setResponseMessage('');
223 setResponseSuccess(true);
224 if (settings && currentStep.fields) {
225 const atLeastOneFieldIsTrue = currentStep.fields.some((field: { id: string }) => {
226 const setting = settings.find(item => item.id === field.id);
227 const value = setting ? setting.value : false;
228 return value === true || (typeof value === 'string' && value.trim() !== '');
229 });
230 if (atLeastOneFieldIsTrue) {
231 addSuccessStep(currentStep.id);
232 }
233 }
234
235 setCurrentStepIndex(currentStepIndex + 1);
236 setContinueDisabled(false);
237 // If this is the last step, reload the page if this is so configured.
238 if (currentStepIndex + 1 >= steps.length && onboardingData.reload_on_finish ) {
239 if (wizardCompletionPromise) {
240 await wizardCompletionPromise;
241 }
242 window.location.reload();
243 }
244 };
245
246 useEffect(() => {
247 setOpen(true);
248 }, [setOpen]);
249
250 if (!currentStep) {
251 return null;
252 }
253
254 return (
255 <ErrorBoundary>
256 <div id="onboarding-modal-root"></div>
257
258 <Modal
259 content={
260 <div className="flex flex-col gap-2 mb-3">
261 <TooltipContainerProvider container={tooltipContainerRef.current}>
262 <div ref={tooltipContainerRef} className="flex flex-col gap-2 justify-center items-stretch">
263
264 <ModalTopBar
265 currentStep={currentStep}
266 handlePrevious={handlePrevious}
267 />
268
269 <ModalHeader
270 currentStep={currentStep}
271 logo={onboardingData.logo}
272 logo_class={`${onboardingData.prefix}-logo`}
273 currentStepIndex={currentStepIndex}
274 />
275
276 {
277 // Hide ModalContent when all plugins are already installed
278 !(currentStep.type === 'plugins' && onboardingData.is_all_plugins_installed) && (
279 <ModalContent
280 step={currentStep}
281 settings={settings}
282 onFieldChange={changeFieldValue}
283 />
284 )
285 }
286 {/* Global response message is always shown for non-connection-test related responses */}
287 {responseCode !== 'BADAUTHPWD' && responseMessage && (
288 <Alert
289 variant={responseSuccess ? 'success' : 'danger'}
290 title={responseSuccess ? "Success" : "Server issues"}
291 message={responseMessage}
292 />
293 )}
294
295
296 <ModalFooter
297 isContinueDisabled={isContinueDisabled}
298 validateAndContinue={validateAndContinue}
299 currentStep={currentStep}
300 handleContinue={handleContinue}
301 />
302
303 </div>
304 </TooltipContainerProvider>
305 </div>
306 }
307 isOpen={isOpen}
308 onClose={handleClose}
309 footer={null}
310 triggerClassName=""
311 children={null}
312 />
313 {isExploding && ConfettiExplosion && <div className="absolute top-1/4 left-1/2 -translate-x-1/2"><ConfettiExplosion duration={4000} width={1400} particleCount={200} force={0.7} zIndex={999999}/></div>}
314 </ErrorBoundary>
315 );
316 };
317
318 export default memo(Onboarding);