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

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