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

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