PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.1.3, at src/AutoLaunch/hooks/useCreateSite.js

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