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

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