PluginProbe
Extendify / 3.0.4
Extendify v3.0.4
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 / AutoLaunch / hooks / useCreateSite.js

useCreateSite.js in Extendify 3.0.4, at src/AutoLaunch/hooks/useCreateSite.js

584 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { handleDesignBuild } from '@auto-launch/fetchers/get-design-build';
2 import { handleHome } from '@auto-launch/fetchers/get-home';
3 import { handleSiteImages } from '@auto-launch/fetchers/get-images';
4 import { handleSiteLogo } from '@auto-launch/fetchers/get-logo';
5 import { handlePages } from '@auto-launch/fetchers/get-pages';
6 import { handleSitePlugins } from '@auto-launch/fetchers/get-plugins';
7 import { handleSiteProfile } from '@auto-launch/fetchers/get-profile';
8 import { handleSiteStrings } from '@auto-launch/fetchers/get-strings';
9 import { handleSiteStyle } from '@auto-launch/fetchers/get-style';
10 import {
11 installFontFamilies,
12 mergeFontsIntoVariation,
13 } from '@auto-launch/functions/fonts';
14 import { apiFetchWithTimeout, setStatus } from '@auto-launch/functions/helpers';
15 import { checkIn } from '@auto-launch/functions/insights';
16 import {
17 updateButtonLinks,
18 updateSinglePageLinksToSections,
19 } from '@auto-launch/functions/links';
20 import {
21 addPageLinksToNav,
22 addSectionLinksToNav,
23 createNavigation,
24 updateNavAttributes,
25 } from '@auto-launch/functions/nav';
26 import {
27 addImprintPage,
28 createWpPages,
29 getPagesToCreate,
30 PLUGIN_OWNED_PAGES,
31 setHelloWorldFeaturedImage,
32 updatePageTitlePattern,
33 } from '@auto-launch/functions/pages';
34 import { generatePageContent } from '@auto-launch/functions/patterns';
35 import {
36 activatePlugin,
37 alreadyActive,
38 getActivePlugins,
39 installPlugin,
40 replacePlaceholderPatterns,
41 } from '@auto-launch/functions/plugins';
42 import {
43 postLaunchFunctions,
44 prefetchAssistData,
45 } from '@auto-launch/functions/setup';
46 import {
47 setThemeRenderingMode,
48 updateTemplatePart,
49 updateVariation,
50 } from '@auto-launch/functions/theme';
51 import { computeVibeAdjustedBlocks } from '@auto-launch/functions/vibes';
52 import {
53 createBlogSampleData,
54 getOption,
55 getPageById,
56 updateOption,
57 } from '@auto-launch/functions/wp';
58 import { useWarnOnLeave } from '@auto-launch/hooks/useWarnOnLeave';
59 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
60 import { useAIConsentStore } from '@shared/state/ai-consent';
61 import { useEffect, useRef, useState } from '@wordpress/element';
62 import { __ } from '@wordpress/i18n';
63 import useSWRImmutable from 'swr/immutable';
64
65 const { homeUrl, showImprint, wpLanguage, installedPluginsSlugs } =
66 window.extSharedData;
67
68 // TODO: I think a good strategy is "if something fails, try to refetch some state"
69
70 export const useCreateSite = () => {
71 // All the data we need to finish
72 const { setErrorMessage, addStatusMessage, needToStall, ...data } =
73 useLaunchDataStore();
74 const { setUserGaveConsent } = useAIConsentStore();
75 const homeStretch = useRef(false);
76 const [warnOnReload, setWarnOnReload] = useState(!needToStall());
77 const [done, setDone] = useState(false);
78
79 // We keep the data on reload but show this to prevent movement
80 useWarnOnLeave(warnOnReload, () => {
81 checkIn({ stage: 'exit_early' });
82 });
83
84 // needs: urlParams['build-id']
85 // provides: designBuild, siteProfile, siteStyle
86 useRunStep(
87 'designBuild',
88 () => {
89 if (!data.urlParams?.['build-id']) return null;
90 return data;
91 },
92 handleDesignBuild,
93 );
94
95 // needs: title, descriptionRaw (or designBuild when build-id)
96 // provides: siteProfile: { type, category, description, title, keywords, logoObjectName }
97 useRunStep(
98 'siteProfile',
99 () => {
100 if (!data.descriptionRaw && !data.title) return null;
101 return data;
102 },
103 async (params) => {
104 checkIn({ stage: 'get_profile' });
105 return await handleSiteProfile(params);
106 },
107 );
108
109 // needs: siteProfile
110 // provides: logoUrl
111 useRunStep(
112 'siteLogo',
113 () => {
114 if (!data.siteProfile?.title) return null;
115 return data;
116 },
117 async (params) => {
118 checkIn({ stage: 'get_logo' });
119 return await handleSiteLogo(params);
120 },
121 );
122
123 // needs: siteProfile
124 // provides:sitePlugins: [{name, wordpressSlug}]
125 useRunStep(
126 'sitePlugins',
127 () => {
128 // We just need the site profile, which has this
129 if (!data.siteProfile?.title) return null;
130 return data;
131 },
132 async (params) => {
133 checkIn({ stage: 'get_plugins' });
134 return await handleSitePlugins(params);
135 },
136 );
137
138 useEffect(() => {
139 // Start installing the partner plugins asap
140 if (!data.sitePlugins) return;
141
142 const pluginsToInstall = data.sitePlugins.filter(
143 ({ wordpressSlug: slug }) => !installedPluginsSlugs?.includes(slug),
144 );
145 if (pluginsToInstall.length === 0) return;
146 setStatus(
147 // translators: this is for a action log UI. Keep it short
148 __('Setting up functionality for your website', 'extendify-local'),
149 );
150 (async function install() {
151 for (const { wordpressSlug: slug } of pluginsToInstall) {
152 const p = await installPlugin(slug);
153 await activatePlugin(p?.plugin ?? slug);
154 }
155 })();
156 }, [data.sitePlugins]);
157
158 // needs: siteProfile
159 // provides: style: {}
160 useRunStep(
161 'siteStyle',
162 () => {
163 // We just need the site profile, which has this
164 if (!data.siteProfile?.title) return null;
165 return data;
166 },
167 async (params) => {
168 checkIn({ stage: 'get_style' });
169 return await handleSiteStyle(params);
170 },
171 );
172
173 // needs: siteProfile
174 // provides: aiHeaders: [], aiBlogTitles: []
175 useRunStep(
176 'siteStrings',
177 () => {
178 // We just need the site profile, which has this
179 if (!data.siteProfile?.title) return null;
180 return data;
181 },
182 async (params) => {
183 checkIn({ stage: 'get_strings' });
184 return await handleSiteStrings(params);
185 },
186 );
187
188 // needs: siteProfile
189 // provides: siteImages: []
190 useRunStep(
191 'siteImages',
192 () => {
193 // We just need the site profile, which has this
194 if (!data.siteProfile?.title) return null;
195 return data;
196 },
197 async (params) => {
198 checkIn({ stage: 'get_images' });
199 return await handleSiteImages(params);
200 },
201 );
202
203 // needs: siteProfile, sitePlugins, siteStyle, siteImages, aiHeaders
204 // provides: home: { id, slug, patterns, siteStyle }
205 useRunStep(
206 'home',
207 () => {
208 // Checking various data from calls above
209 const ok = [
210 data.siteProfile,
211 data.siteStyle,
212 data.siteImages,
213 data.sitePlugins,
214 data.aiHeaders,
215 ].every((v) => v !== undefined);
216 return ok ? data : null;
217 },
218 async (params) => {
219 checkIn({ stage: 'get_home' });
220 return await handleHome(params);
221 },
222 );
223
224 // siteProfile, sitePlugins, siteStyle, siteImages
225 // provides: pages: [{ id, slug, name, patterns, siteStyle }]
226 useRunStep(
227 'pages',
228 () => {
229 // Checking various data from calls above
230 const ok = [
231 data.siteProfile,
232 data.siteStyle,
233 data.siteImages,
234 data.sitePlugins,
235 ].every((v) => v !== undefined);
236 return ok ? data : null;
237 },
238 async (params) => {
239 checkIn({ stage: 'get_pages' });
240 return await handlePages(params);
241 },
242 );
243
244 // basic defaults
245 useRunStep(
246 'generalUpdates',
247 () => {
248 const ok = [data.siteProfile, data.home, data.pages].every(
249 (v) => v !== undefined,
250 );
251 return ok ? data : null;
252 },
253 async ({ siteProfile, sitePlugins, siteStyle }) => {
254 checkIn({ stage: 'set_general', siteProfile, sitePlugins, siteStyle });
255
256 const { title } = siteProfile;
257 // translators: this is for a action log UI. Keep it short
258 addStatusMessage(__('Adding admin configurations', 'extendify-local'));
259 // update permalinks
260 await updateOption('permalink_structure', '/%postname%/');
261 // make sure consent is set
262 setUserGaveConsent(true);
263 // Update title
264 if (title) await updateOption('blogname', title);
265 },
266 );
267
268 // basic defaults
269 useRunStep(
270 'pluginConfigurations',
271 () => {
272 if (!data.sitePlugins) return null;
273 return data;
274 },
275 async () => {
276 checkIn({ stage: 'set_plugin_config' });
277 const activePlugins = await getActivePlugins();
278 if (alreadyActive(activePlugins, 'wpforms-lite')) {
279 await updateOption('wpforms_activation_redirect', 'skip');
280 }
281 if (alreadyActive(activePlugins, 'all-in-one-seo-pack')) {
282 await updateOption('aioseo_activation_redirect', 'skip');
283 }
284 if (alreadyActive(activePlugins, 'google-analytics-for-wordpress')) {
285 const param = '_transient__monsterinsights_activation_redirect';
286 await updateOption(param, null);
287 }
288 },
289 );
290
291 // If we have home and (maybe) pages then we're ready
292 useEffect(() => {
293 if (needToStall()) return;
294 const {
295 home,
296 pages,
297 siteProfile,
298 sitePlugins,
299 siteStyle,
300 aiBlogTitles,
301 siteImages,
302 designBuild,
303 } = data;
304 // pages could be [] and pass here, that's ok
305 if (!home || !pages) return;
306 if (homeStretch.current) return;
307 homeStretch.current = true;
308 (async () => {
309 const { objective, structure, category } = siteProfile;
310
311 // Do they need an imprint page?
312 const needsImprint = Array.isArray(showImprint)
313 ? showImprint.includes(wpLanguage ?? '') && category === 'Business'
314 : false;
315
316 const customFonts =
317 siteStyle?.variation?.settings?.typography?.fontFamilies?.custom;
318 let variation = siteStyle?.variation;
319 if (customFonts?.length) {
320 checkIn({ stage: 'install_fonts' });
321 // translators: this is for a action log UI. Keep it short
322 addStatusMessage(__('Installing fonts locally', 'extendify-local'));
323 const installed = await installFontFamilies(customFonts).catch(
324 () => [],
325 );
326 variation = mergeFontsIntoVariation(siteStyle.variation, installed);
327 }
328
329 if (siteStyle?.vibe && siteStyle.vibe !== 'natural-1') {
330 // translators: vibe in this context is a noun - the feeling of their site design.
331 addStatusMessage(__('Setting the website style', 'extendify-local'));
332 checkIn({ stage: 'compute_vibe' });
333 const vibeBlocks = await computeVibeAdjustedBlocks(
334 siteStyle.vibe,
335 ).catch(() => null);
336 if (vibeBlocks) {
337 variation = {
338 ...variation,
339 styles: { ...variation.styles, blocks: vibeBlocks },
340 };
341 }
342 }
343
344 checkIn({ stage: 'set_vibe' });
345 await updateVariation(variation);
346
347 // navigation menu
348 addStatusMessage(__('Working on the navigation', 'extendify-local'));
349 const { id: headerNavId } = await createNavigation({
350 title: __('Header Navigation', 'extendify-local'),
351 slug: 'site-navigation',
352 });
353 let headerCode = updateNavAttributes(home.headerCode || '', {
354 ref: headerNavId,
355 });
356 // remove the header navigation from the landing page
357 if (objective === 'landing-page') {
358 // translators: this is for a action log UI. Keep it short
359 addStatusMessage(__('Perfecting a landing page', 'extendify-local'));
360 const social =
361 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis;
362 headerCode = headerCode
363 .replace(/<!--\s*wp:navigation\b[^>]*.*\/-->/gis, '')
364 .replace(social, '');
365 }
366 if (typeof siteProfile.phoneNumber === 'string') {
367 headerCode = headerCode.replaceAll(
368 // Hardcoded in the template
369 '206-555-0100',
370 siteProfile.phoneNumber ||
371 // translators: Use a number that is appropriate for the locale. It does not need to be this exact number. This is a placeholder phone number. For example, in pt_BR you could use (11) 91234-5678.
372 __('206-555-0100', 'extendify-local'),
373 );
374 }
375 checkIn({ stage: 'set_navigation' });
376 await updateTemplatePart('extendable/header', headerCode);
377
378 // footer
379 let footerNavId = null;
380 let footerCode = home.footerCode || '';
381 if (needsImprint) {
382 const nav = await createNavigation({
383 title: __('Footer Navigation', 'extendify-local'),
384 slug: 'footer-navigation',
385 });
386 footerNavId = nav.id;
387 footerCode = updateNavAttributes(footerCode, { ref: footerNavId });
388 }
389 checkIn({ stage: 'set_footer' });
390 await updateTemplatePart('extendable/footer', footerCode);
391
392 // pages
393 // translators: this is for a action log UI. Keep it short
394 addStatusMessage(__('Creating pages', 'extendify-local'));
395 const pagesToCreate = getPagesToCreate(data);
396 const titlePattern = pages?.[0]?.patterns?.find((p) =>
397 p.patternTypes?.includes('page-title'),
398 );
399 if (titlePattern) {
400 checkIn({ stage: 'set_page_title_pattern' });
401 await updatePageTitlePattern(titlePattern.code);
402 }
403
404 const activePlugins = await getActivePlugins();
405 // This lets us keep plugin pages in th enav but skip making the page
406 const reservedSlugs = new Set(
407 PLUGIN_OWNED_PAGES.filter(
408 ({ plugin }) =>
409 sitePlugins.some((p) => p.wordpressSlug === plugin) ||
410 alreadyActive(activePlugins, plugin),
411 ).map(({ slug }) => slug),
412 );
413 const pagesToActuallyCreate = pagesToCreate.filter(
414 (p) => !reservedSlugs.has(p.slug),
415 );
416
417 // Some patterns have preview html, we can replace those
418 // which may install some plugins too.
419 const pagesReplaced = [];
420 // Run these one page at a time so we don't end up
421 // with duplicate dependency issues
422 checkIn({ stage: 'replace_placeholder_patterns' });
423 for (const page of pagesToActuallyCreate) {
424 const patterns = await replacePlaceholderPatterns(page.patterns);
425 const updatedPage = { ...page, patterns };
426 pagesReplaced.push(updatedPage);
427 }
428 checkIn({ stage: 'generate_page_content' });
429 const customPages = await generatePageContent(pagesReplaced, data);
430
431 const stickyNav =
432 structure === 'single-page' && objective !== 'landing-page';
433 const createdPagesWP = await createWpPages(customPages, { stickyNav });
434 // Aux pages
435 const hasBlogPattern = home?.patterns?.some((pattern) =>
436 pattern.patternTypes.includes('blog-section'),
437 );
438 if (objective === 'blog' || hasBlogPattern) {
439 checkIn({ stage: 'create_blog_sample_data' });
440 // translators: this is for a action log UI. Keep it short
441 addStatusMessage(__('Creating blog sample data', 'extendify-local'));
442 await createBlogSampleData({ aiBlogTitles }, siteImages);
443 }
444 // If we have site images then set up the hello world image
445 if (siteImages?.length) {
446 checkIn({ stage: 'set_hello_world_image' });
447 await setHelloWorldFeaturedImage(siteImages);
448 }
449
450 let imprint = {};
451 if (needsImprint) {
452 checkIn({ stage: 'create_imprint' });
453 imprint = await addImprintPage({ siteStyle }).catch(() => null);
454 }
455
456 const pluginPages = [];
457 if (alreadyActive(activePlugins, 'woocommerce')) {
458 checkIn({ stage: 'import_woocommerce_products' });
459 addStatusMessage(
460 // translators: this is for a action log UI. Keep it short
461 __('Setting up your online store', 'extendify-local'),
462 );
463 await apiFetchWithTimeout({
464 path: '/extendify/v1/auto-launch/import-woocommerce',
465 }).catch(() => null);
466 const id = await getOption('woocommerce_shop_page_id');
467 const shopPage = id ? await getPageById(id) : null;
468 if (shopPage) pluginPages.push(shopPage);
469 }
470 if (alreadyActive(activePlugins, 'the-events-calendar')) {
471 pluginPages.push({
472 title: { rendered: __('Events', 'extendify-local') },
473 slug: 'events',
474 link: `${homeUrl}/events`,
475 });
476 }
477
478 // Adding pages to the nav
479 checkIn({ stage: 'set_page_links' });
480 const pagesWithLinksUpdated =
481 structure === 'single-page'
482 ? await updateSinglePageLinksToSections(createdPagesWP, customPages, {
483 objective,
484 activePlugins,
485 landingPageCTALink: siteProfile.landingPageCTALink,
486 })
487 : await updateButtonLinks(createdPagesWP, pluginPages);
488 const footerNavPages = [];
489 if (footerNavId && imprint?.title) {
490 const { originalSlug, title } = imprint;
491 footerNavPages.push({
492 id: originalSlug,
493 name: title.rendered,
494 slug: originalSlug,
495 patterns: [],
496 });
497 }
498
499 checkIn({ stage: 'set_navigation_links' });
500 if (objective !== 'landing-page') {
501 const orderedSlugs = designBuild?.pages?.map((p) => p.slug) ?? [];
502 if (structure === 'single-page') {
503 await addSectionLinksToNav(
504 headerNavId,
505 home?.patterns,
506 pluginPages,
507 createdPagesWP,
508 { orderedSlugs },
509 );
510 } else {
511 await addPageLinksToNav(
512 headerNavId,
513 pagesToCreate,
514 pagesWithLinksUpdated,
515 pluginPages,
516 { orderedSlugs },
517 );
518 }
519 if (footerNavId) {
520 await addPageLinksToNav(
521 footerNavId,
522 footerNavPages,
523 imprint?.id
524 ? [...pagesWithLinksUpdated, imprint]
525 : pagesWithLinksUpdated,
526 [],
527 );
528 }
529 }
530
531 checkIn({ stage: 'prefetch_assist_data' });
532 await prefetchAssistData();
533 checkIn({ stage: 'final_steps' });
534 await setThemeRenderingMode('template-locked');
535 await postLaunchFunctions();
536 // translators: this is for a action log UI. Keep it short
537 addStatusMessage(__('All done!', 'extendify-local'));
538 await checkIn({ stage: 'finished', siteProfile, sitePlugins, siteStyle });
539 setWarnOnReload(false);
540 setDone(true);
541 })().catch((error) => {
542 console.error(error);
543 // if we error here we can try again by resetting the home stretch and stalling again to refetch data
544 homeStretch.current = false;
545 needToStall(true);
546 setErrorMessage(
547 __(
548 'Something went wrong during the final steps. We will try again but you may need to refresh the page.',
549 'extendify-local',
550 ),
551 );
552 });
553 }, [data, needToStall, setUserGaveConsent]);
554
555 return { done };
556 };
557
558 const useRunStep = (stepKey, getParams, fetcher) => {
559 const { setData, setErrorMessage, needToStall } = useLaunchDataStore();
560 const p = getParams?.() ?? null;
561 const { data, error } = useSWRImmutable(
562 p && !needToStall() ? stepKey : null,
563 () => fetcher(getParams()),
564 );
565
566 useEffect(() => {
567 if (!data) return;
568 Object.entries(data).forEach(([k, v]) => {
569 setData(k, v);
570 });
571 }, [data, setData]);
572
573 useEffect(() => {
574 if (!error || needToStall()) return;
575 console.error(error);
576 setErrorMessage(
577 __(
578 'Having some trouble with this step. Trying again...',
579 'extendify-local',
580 ),
581 );
582 }, [error, setErrorMessage, needToStall]);
583 };
584