import { Transition } from '@headlessui/react'; import { getPageTemplates } from '@launch/api/DataApi'; import { importTemporaryProducts } from '@launch/api/WooCommerce'; import { addPageLinksToNav, addSectionLinksToNav, createNavigation, getActivePlugins, getOption, getPageById, installFontFamilies, postLaunchFunctions, prefetchAssistData, updateNavAttributes, updateOption, updatePageTitlePattern, updateTemplatePart, } from '@launch/api/WPApi'; import { PagesSkeleton } from '@launch/components/CreatingSite/PageSkeleton'; import { useConfetti } from '@launch/hooks/useConfetti'; import { useSiteLogo } from '@launch/hooks/useSiteLogo'; import { useWarnOnLeave } from '@launch/hooks/useWarnOnLeave'; import { updateButtonLinks, updateSinglePageLinksToSections, } from '@launch/lib/linkPages'; import { uploadLogo } from '@launch/lib/logo'; import { addImprintPage, createBlogSampleData, createWpPages, generateCustomPageContent, replacePlaceholderPatterns, setHelloWorldFeaturedImage, updateGlobalStyleVariant, updateNaturalVibeStyles, } from '@launch/lib/wp'; import { usePagesStore } from '@launch/state/Pages'; import { usePagesSelectionStore } from '@launch/state/pages-selections'; import { useUserSelectionStore } from '@launch/state/user-selections'; import { Logo, Spinner } from '@launch/svg'; import { buildRecommendedPagesParams } from '@launch/utils/buildRecommendedPagesParams'; import { getPartnerPlugins, recordPluginActivity } from '@shared/api/DataApi'; import { activatePlugin, installPlugin } from '@shared/api/wp'; import { pageNames } from '@shared/lib/pages'; import { deepMerge, retryOperation, waitFor200Response, wasPluginInstalled, } from '@shared/lib/utils'; import { useAIConsentStore } from '@shared/state/ai-consent'; import { dispatch, select } from '@wordpress/data'; import { useCallback, useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { colord } from 'colord'; const { homeUrl, adminUrl, partnerLogo, partnerName, showImprint, wpLanguage, installedPluginsSlugs, } = window.extSharedData; export const CreatingSite = () => { const [isShowing] = useState(true); const [confettiReady, setConfettiReady] = useState(false); const [confettiColors, setConfettiColors] = useState(['#ffffff']); const [warnOnLeaveReady, setWarnOnLeaveReady] = useState(true); const [loadAdmin, setLoadAdmin] = useState(false); const { siteType, siteInformation, siteStructure, sitePlugins, variation, siteProfile, siteStrings, siteImages, CTALink, siteObjective, siteQA, urlParameters, setSiteStrings, } = useUserSelectionStore(); const { pages, style, removeAll, add } = usePagesSelectionStore(); const [info, setInfo] = useState([ __('Preparing to build your site', 'extendify-local'), ]); const [infoDesc, setInfoDesc] = useState([ // Because useSiteLogo causes a noticeable delay, we set this initial desc to match __('Generating a site logo', 'extendify-local'), ]); const inform = (msg) => setInfo((info) => [msg, ...info]); const informDesc = (msg) => setInfoDesc((infoDesc) => [msg, ...infoDesc]); const [pagesToAnimate, setPagesToAnimate] = useState([]); const { setPage } = usePagesStore(); const customFontFamilies = variation?.settings?.typography?.fontFamilies?.custom; const { setUserGaveConsent } = useAIConsentStore(); const { loading: logoLoading, logoUrl } = useSiteLogo(); const redirectUrl = // on landing pages for some users, we redirect to home_url (window.extOnbData?.redirectToWebsite && siteObjective === 'landing-page') || window.extSharedData?.showAIAgents ? `${homeUrl}?extendify-launch-success=1` : `${adminUrl}admin.php?page=extendify-assist&extendify-launch-success=1`; const shouldLoadPages = urlParameters?.skip?.includes('pages') && siteStructure === 'multi-page'; const [pagesLoaded, setPagesLoaded] = useState(false); useWarnOnLeave(warnOnLeaveReady); const loadRecommendedPages = useCallback(async () => { const recommended = await getPageTemplates(buildRecommendedPagesParams()); removeAll('pages'); recommended.recommended.forEach((page) => { add('pages', page); }); setPagesLoaded(true); }, [removeAll, add]); useEffect(() => { if (!shouldLoadPages) return; if (pagesLoaded) return; loadRecommendedPages().catch(console.error); }, [shouldLoadPages, pagesLoaded, loadRecommendedPages]); const doEverything = useCallback(async () => { try { const blogQuestion = siteQA?.questions?.find( (question) => question.id === 'blog', ); const hasBlogGoal = blogQuestion ? (blogQuestion?.answerUser ?? blogQuestion?.answerAI) === 'yes' : siteObjective === 'blog' || false; const needsImprintPage = Array.isArray(showImprint) ? showImprint.includes(wpLanguage ?? '') && siteProfile?.aiSiteCategory === 'Business' : false; await uploadLogo(logoUrl, { forceReplace: true }); await updateOption('permalink_structure', '/%postname%/'); await waitFor200Response(); inform(__('Applying your website styles', 'extendify-local')); informDesc(__('Creating a beautiful website', 'extendify-local')); await new Promise((resolve) => setTimeout(resolve, 1000)); // If they are launching the site, it means they agreed to the terms setUserGaveConsent(true); if (siteInformation.title) { await updateOption('blogname', siteInformation.title); } await waitFor200Response(); // TODO: Refactor to assume 0default for site type const siteTypeUpdated = { ...(siteType ?? {}), // Override with the ai site type if it exists name: siteProfile?.aiSiteType ?? siteType.name, }; await updateOption( 'extendify_siteType', // Only persist the site type if the slug exists siteType?.slug ? siteTypeUpdated : {}, ); await waitFor200Response(); // Install font families that are not in the theme. if (customFontFamilies?.length) { const installedFontFamilies = await installFontFamilies(customFontFamilies); await updateGlobalStyleVariant( deepMerge( variation, // We set to null first to reset the field. { settings: { typography: { fontFamilies: { custom: null } } } }, // We add the installed font families here to activate them. { settings: { typography: { fontFamilies: { custom: installedFontFamilies.filter(Boolean), }, }, }, }, ) ?? {}, ); } else { await updateGlobalStyleVariant(variation); } await waitFor200Response(); const selectedVibe = style?.siteStyle?.vibe; const selectedAnimation = style?.siteStyle?.animation; if (selectedVibe && selectedVibe !== 'natural-1') { inform( // translators: "site style" refers to the structural aesthetic style for the site. __('Applying site style', 'extendify-local'), ); informDesc( // translators: "site style" refers to the structural aesthetic style for the site. __('Customizing your site style', 'extendify-local'), ); await updateNaturalVibeStyles(selectedVibe); await new Promise((resolve) => setTimeout(resolve, 1000)); } const navigationId = await createNavigation(); let headerCode = updateNavAttributes(style?.headerCode, { ref: navigationId, }); if (siteObjective === 'landing-page') { // remove the header navigation from the landing page headerCode = headerCode .replace(//gis, '') .replace( //gis, '', ); } let footerCode = style?.footerCode; let footerNavigationId = null; let footerNavPages = []; if (needsImprintPage) { footerNavigationId = await createNavigation( 'content', __('Footer Navigation', 'extendify-local'), 'footer-navigation', ); footerCode = updateNavAttributes(footerCode, { ref: footerNavigationId, }); } await waitFor200Response(); await updateTemplatePart('extendable/header', headerCode); await waitFor200Response(); await updateTemplatePart('extendable/footer', footerCode); inform(__('Populating data', 'extendify-local')); informDesc(__('Personalizing your experience', 'extendify-local')); await prefetchAssistData(); await waitFor200Response(); await new Promise((resolve) => setTimeout(resolve, 1000)); // Store the site vibes, colorPalette and fonts await updateOption( 'extendify_siteStyle', style?.siteStyle || { vibe: 'standard', fonts: { heading: {}, body: {} }, colorPalette: null, }, ); // Store animation settings if present if (selectedAnimation) { await waitFor200Response(); // Handle both string and object formats const animationSettings = typeof selectedAnimation === 'string' ? { type: selectedAnimation, speed: 'medium' } : selectedAnimation; await updateOption('extendify_animation_settings', animationSettings); } // Add required plugins to the end of the list to give them lower priority // when filtering out duplicates. const sortedPlugins = [...sitePlugins] // Remove duplicates .reduce((acc, plugin) => { const found = acc.find( ({ wordpressSlug: s }) => s === plugin.wordpressSlug, ); if (!found) acc.push(plugin); return acc; }, []) // We add give to the front. See here why: // https://github.com/extendify/company-product/issues/713 .sort(({ wordpressSlug }) => (wordpressSlug === 'give' ? -1 : 1)); if (sortedPlugins?.length) { inform(__('Preparing site functionality', 'extendify-local')); await new Promise((resolve) => setTimeout(resolve, 1000)); const pluginInstallMessages = [ __('Getting everything ready', 'extendify-local'), __('Enhancing your site', 'extendify-local'), __('Setting up essential tools', 'extendify-local'), ]; informDesc(pluginInstallMessages?.at(-1)); for (const [index, plugin] of sortedPlugins.entries()) { const slug = plugin?.wordpressSlug; // Don't install if already installed if (!installedPluginsSlugs?.includes(slug)) { await retryOperation(() => installPlugin(slug), { maxAttempts: 2, }).catch(console.error); recordPluginActivity({ slug, source: 'launch' }); if (index % 2 === 1) { // skip first message const i = Math.floor(index / 2) % pluginInstallMessages.length; informDesc(pluginInstallMessages[i]); } } await retryOperation(() => activatePlugin(slug), { maxAttempts: 2, }).catch(console.error); } } const homePage = { name: pageNames.home.title, id: 'home', patterns: style.patterns, slug: 'home', }; const blogPage = { name: pageNames.blog.title, id: 'blog', patterns: [], slug: 'blog', }; await waitFor200Response(); inform(__('Adding page content', 'extendify-local')); informDesc(__('Starting off with a full website', 'extendify-local')); await new Promise((resolve) => setTimeout(resolve, 1000)); await waitFor200Response(); if (siteProfile.aiDescription) { informDesc(__('Creating pages with custom content', 'extendify-local')); [homePage, ...pages].forEach((page) => { setPagesToAnimate((previous) => [...previous, page.name]); }); } const pagesWithoutPageTitlePattern = pages.map((page) => ({ ...page, patterns: page.patterns.filter( (p) => !p.patternTypes?.includes('page-title'), ), })); // Update the page-with-title template with the selected page-title pattern const firstPageTitlePattern = pages?.[0]?.patterns?.find((p) => p.patternTypes?.includes('page-title'), ); const hasPageWithTitleTemplate = firstPageTitlePattern ? await updatePageTitlePattern(firstPageTitlePattern.code) : false; const pagesToUse = hasPageWithTitleTemplate ? pagesWithoutPageTitlePattern : pages; const pagesToCreate = [ ...pagesToUse, homePage, hasBlogGoal ? blogPage : null, ].filter(Boolean); const pagesWithReplacedPatterns = []; // Run these one page at a time so we don't end up with duplicate dependency issues for (const page of pagesToCreate) { const updatedPage = { ...page, patterns: await replacePlaceholderPatterns(page.patterns), }; pagesWithReplacedPatterns.push(updatedPage); } const pagesWithCustomContent = await generateCustomPageContent( pagesWithReplacedPatterns, { sitePlugins, siteType: siteTypeUpdated.name, siteInformation, }, siteProfile, ); const createdHomePage = pagesWithCustomContent.find( (p) => p.slug === 'home', ); const heroPattern = createdHomePage?.patterns?.find((p) => p.patternTypes?.includes('hero-header'), ); const pMatch = heroPattern?.code?.match(/
]*>([\s\S]*?)<\/p>/);
const heroDesc = pMatch?.[1]?.replace(/<[^>]+>/g, '').trim();
setSiteStrings({
...siteStrings,
heroDescription: heroDesc || siteStrings?.heroDescription,
});
const createdPages = await createWpPages(pagesWithCustomContent, {
stickyNav:
siteStructure === 'single-page' && siteObjective !== 'landing-page',
});
const hasBlogPattern = homePage?.patterns?.some((pattern) =>
pattern.patternTypes.includes('blog-section'),
);
if (hasBlogGoal || hasBlogPattern) {
informDesc(__('Creating blog sample data', 'extendify-local'));
await createBlogSampleData(siteStrings, siteImages);
}
await waitFor200Response();
if (siteImages?.siteImages) {
await setHelloWorldFeaturedImage(siteImages.siteImages);
}
if (needsImprintPage) {
informDesc(__('Adding imprint page', 'extendify-local'));
const createdImprintPage = await addImprintPage(style?.siteStyle);
if (createdImprintPage) {
createdPages.push(createdImprintPage);
footerNavPages = [
{
name: createdImprintPage.title.rendered,
slug: createdImprintPage.originalSlug,
id: createdImprintPage.originalSlug,
patterns: [],
},
];
}
}
setPagesToAnimate([]);
await waitFor200Response();
informDesc(__('Setting up site layout', 'extendify-local'));
const navPagesMultiPageSite = [
...pages,
hasBlogGoal ? blogPage : null,
homePage,
].filter(Boolean);
const pluginPages = [];
// Fetch active plugins after installing plugins
const { data: activePlugins } = await getActivePlugins();
// Add plugin related pages only if plugin is active
if (wasPluginInstalled(activePlugins, 'woocommerce')) {
const shopPageId = await getOption('woocommerce_shop_page_id');
const shopPage = shopPageId
? await getPageById(shopPageId).catch(() => null)
: null;
if (shopPage) {
pluginPages.push(shopPage);
}
informDesc(__('Importing shop sample data', 'extendify-local'));
try {
await importTemporaryProducts();
} catch (e) {
console.error('Error importing temporary products', e);
}
// If we installed any plugins above, and a partner has supported plugins
// linked to those plugins, we should install them here. For example:
// A German specific WooCommerce plugin in case WooCommerce is installed.
const partnerPlugins = await getPartnerPlugins('products').catch(
() => null,
);
if (partnerPlugins) {
informDesc(__('Installing supporting plugins', 'extendify-local'));
for (const plugin of partnerPlugins) {
if (!wasPluginInstalled(activePlugins, plugin)) {
const maxAttempts = 2;
await retryOperation(() => installPlugin(plugin), {
maxAttempts,
}).catch(console.error);
recordPluginActivity({ slug: plugin, source: 'launch' });
await retryOperation(() => activatePlugin(plugin), {
maxAttempts,
}).catch(console.error);
}
}
}
}
if (wasPluginInstalled(activePlugins, 'the-events-calendar')) {
const eventsPage = {
title: {
rendered: __('Events', 'extendify-local'),
},
slug: 'events',
link: `${homeUrl}/events`,
};
pluginPages.push(eventsPage);
}
if (wasPluginInstalled(activePlugins, 'wpforms-lite')) {
await updateOption('wpforms_activation_redirect', 'skip');
}
if (wasPluginInstalled(activePlugins, 'all-in-one-seo-pack')) {
await updateOption('aioseo_activation_redirect', 'skip');
}
if (wasPluginInstalled(activePlugins, 'google-analytics-for-wordpress')) {
await updateOption(
'_transient__monsterinsights_activation_redirect',
null,
);
}
const pagesWithLinksUpdated =
siteStructure === 'single-page'
? await updateSinglePageLinksToSections(
createdPages,
pagesWithCustomContent,
{
linkOverride: CTALink,
siteObjective,
},
)
: await updateButtonLinks(createdPages, pluginPages);
if (siteObjective !== 'landing-page') {
if (siteStructure === 'single-page') {
await addSectionLinksToNav(
navigationId,
homePage?.patterns,
pluginPages,
createdPages,
);
} else {
await addPageLinksToNav(
navigationId,
navPagesMultiPageSite,
pagesWithLinksUpdated,
pluginPages,
);
}
if (footerNavigationId) {
await addPageLinksToNav(
footerNavigationId,
footerNavPages,
pagesWithLinksUpdated,
[],
);
}
}
await waitFor200Response();
const renderingModes =
select('core/preferences').get('core', 'renderingModes') || {};
if (renderingModes?.extendable?.page !== 'template-locked') {
dispatch('core/preferences').set('core', 'renderingModes', {
...renderingModes,
extendable: {
...(renderingModes.extendable || {}),
page: 'template-locked',
},
});
}
inform(__('Setting up your Site Assistant', 'extendify-local'));
informDesc(__('Helping you to succeed', 'extendify-local'));
await new Promise((resolve) => setTimeout(resolve, 1000));
await waitFor200Response();
inform(__('Your website has been created!', 'extendify-local'));
informDesc(__('Redirecting in 3, 2, 1...', 'extendify-local'));
// fire confetti here
setConfettiReady(true);
setWarnOnLeaveReady(false);
await new Promise((resolve) => setTimeout(resolve, 2500));
await waitFor200Response();
} catch (e) {
console.error(e);
// if the error is 4xx, we should stop trying and prompt them to reload
if (e.status >= 400 && e.status < 500) {
setWarnOnLeaveReady(false);
const alertMsg = __(
'We encountered a server error we cannot recover from. Please reload the page and try again.',
'extendify-local',
);
alert(alertMsg);
location.href = adminUrl;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
return doEverything();
}
}, [
pages,
style,
siteType,
siteInformation,
setPagesToAnimate,
siteStructure,
variation,
siteProfile,
siteStrings,
siteImages,
customFontFamilies,
setUserGaveConsent,
siteObjective,
CTALink,
logoUrl,
sitePlugins,
siteQA,
setSiteStrings,
]);
useEffect(() => {
if (logoLoading) return;
if (shouldLoadPages && !pagesLoaded) return;
doEverything().then(async () => {
setPage(0);
// This will trigger the post launch php functions.
await postLaunchFunctions();
// This loads the admin in the background to run php functions in admin context
setLoadAdmin(true);
await waitFor200Response();
window.location.replace(redirectUrl);
});
}, [
doEverything,
setPage,
logoLoading,
redirectUrl,
shouldLoadPages,
pagesLoaded,
]);
useEffect(() => {
const documentStyles = window.getComputedStyle(document.body);
const partnerBg = documentStyles?.getPropertyValue('--ext-banner-main');
const partnerText = documentStyles?.getPropertyValue('--ext-banner-text');
if (partnerBg) {
setConfettiColors([
colord(partnerBg).darken(0.3).toHex(),
colord(partnerText).alpha(0.5).toHex(),
colord(partnerBg).lighten(0.2).toHex(),
]);
}
}, []);
useConfetti(
{
particleCount: 3,
angle: 320,
spread: 220,
origin: { x: 0, y: 0 },
colors: confettiColors,
},
2500,
confettiReady,
);
return (
<>