PluginProbe
Extendify / trunk
Extendify vtrunk
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
← All changes | src/AutoLaunch/hooks/useCreateSite.js +158 -89 3.0.5 → trunk View file →
@@ -1,7 +1,8 @@
1 1 import { handleDesignBuild } from '@auto-launch/fetchers/get-design-build';
2 2 import { handleHome } from '@auto-launch/fetchers/get-home';
3 3 import { handleSiteImages } from '@auto-launch/fetchers/get-images';
4 +import { handleLaunchDecisions } from '@auto-launch/fetchers/get-launch-decisions';
4 5 import { handleSiteLogo } from '@auto-launch/fetchers/get-logo';
5 6 import { handlePages } from '@auto-launch/fetchers/get-pages';
6 7 import { handleSitePlugins } from '@auto-launch/fetchers/get-plugins';
7 8 import { handleSiteProfile } from '@auto-launch/fetchers/get-profile';
@@ -18,10 +19,13 @@
18 19 updateSinglePageLinksToSections,
19 20 } from '@auto-launch/functions/links';
20 21 import {
21 22 addPageLinksToNav,
23 + addSectionLinksFromDesign,
22 24 addSectionLinksToNav,
23 25 createNavigation,
26 + injectNavExtras,
27 + injectWooCommerceIcons,
24 28 updateNavAttributes,
25 29 } from '@auto-launch/functions/nav';
26 30 import {
27 31 addImprintPage,
@@ -26,8 +30,9 @@
26 30 import {
27 31 addImprintPage,
28 32 createWpPages,
29 33 getPagesToCreate,
34 + isBlogPage,
30 35 PLUGIN_OWNED_PAGES,
31 36 setHelloWorldFeaturedImage,
32 37 updatePageTitlePattern,
33 38 } from '@auto-launch/functions/pages';
@@ -32,46 +37,49 @@
32 37 updatePageTitlePattern,
33 38 } from '@auto-launch/functions/pages';
34 39 import { generatePageContent } from '@auto-launch/functions/patterns';
35 40 import {
36 - activatePlugin,
37 41 alreadyActive,
42 + ensurePluginsActive,
38 43 getActivePlugins,
39 - installPlugin,
40 44 replacePlaceholderPatterns,
45 + reportFailedDependencies,
46 + reportInactivePlugins,
41 47 } from '@auto-launch/functions/plugins';
42 48 import {
43 49 postLaunchFunctions,
44 50 prefetchAssistData,
45 51 } from '@auto-launch/functions/setup';
52 +import { applySocialProfiles } from '@auto-launch/functions/social-links';
46 53 import {
47 54 setThemeRenderingMode,
48 55 updateTemplatePart,
49 56 updateVariation,
50 57 } from '@auto-launch/functions/theme';
51 -import { computeVibeAdjustedBlocks } from '@auto-launch/functions/vibes';
58 +import { computeVibeAdjustments } from '@auto-launch/functions/vibes';
52 59 import {
53 60 createBlogSampleData,
54 61 getOption,
55 62 getPageById,
63 + storeSiteImages,
56 64 updateOption,
57 65 } from '@auto-launch/functions/wp';
58 66 import { useWarnOnLeave } from '@auto-launch/hooks/useWarnOnLeave';
59 67 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
68 +import { launchStrings } from '@auto-launch/strings';
60 69 import { digest } from '@shared/api/digest';
70 +import { siteImageUrls } from '@shared/lib/site-images';
61 71 import { useAIConsentStore } from '@shared/state/ai-consent';
62 72 import { useEffect, useRef, useState } from '@wordpress/element';
63 73 import { __ } from '@wordpress/i18n';
64 74 import useSWRImmutable from 'swr/immutable';
65 75
66 -const { homeUrl, showImprint, wpLanguage, installedPluginsSlugs } =
67 - window.extSharedData;
76 +const { homeUrl, showImprint, wpLanguage } = window.extSharedData;
68 77
69 78 // TODO: I think a good strategy is "if something fails, try to refetch some state"
70 79
71 80 export const useCreateSite = () => {
72 - // All the data we need to finish
73 - const { setErrorMessage, addStatusMessage, needToStall, ...data } =
81 + const { setErrorMessage, addStatusMessage, needToStall, setData, ...data } =
74 82 useLaunchDataStore();
75 83 const { setUserGaveConsent } = useAIConsentStore();
76 84 const homeStretch = useRef(false);
77 85 const [warnOnReload, setWarnOnReload] = useState(!needToStall());
@@ -112,8 +120,11 @@
112 120 useRunStep(
113 121 'siteLogo',
114 122 () => {
115 123 if (!data.siteProfile?.title) return null;
124 + // Logo was already uploaded by handleDesignBuild;
125 + // generating an AI logo here is not necessary.
126 + if (data.designBuild?.logoUrl) return null;
116 127 return data;
117 128 },
118 129 async (params) => {
119 130 checkIn({ stage: 'get_logo' });
@@ -121,13 +132,12 @@
121 132 },
122 133 );
123 134
124 135 // needs: siteProfile
125 - // provides:sitePlugins: [{name, wordpressSlug}]
136 + // provides: sitePlugins: [{name, wordpressSlug}]
126 137 useRunStep(
127 138 'sitePlugins',
128 139 () => {
129 - // We just need the site profile, which has this
130 140 if (!data.siteProfile?.title) return null;
131 141 return data;
132 142 },
133 143 async (params) => {
@@ -137,24 +147,17 @@
137 147 );
138 148
139 149 useEffect(() => {
140 150 // Start installing the partner plugins asap
141 - if (!data.sitePlugins) return;
151 + if (!data.sitePlugins?.length) return;
142 152
143 - const pluginsToInstall = data.sitePlugins.filter(
144 - ({ wordpressSlug: slug }) => !installedPluginsSlugs?.includes(slug),
145 - );
146 - if (pluginsToInstall.length === 0) return;
147 153 setStatus(
148 154 // translators: this is for a action log UI. Keep it short
149 - __('Setting up functionality for your website', 'extendify-local'),
155 + launchStrings().statusFunctionality,
150 156 );
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 + ensurePluginsActive(
158 + data.sitePlugins.map(({ wordpressSlug }) => wordpressSlug),
159 + );
157 160 }, [data.sitePlugins]);
158 161
159 162 // needs: siteProfile
160 163 // provides: style: {}
@@ -160,9 +163,8 @@
160 163 // provides: style: {}
161 164 useRunStep(
162 165 'siteStyle',
163 166 () => {
164 - // We just need the site profile, which has this
165 167 if (!data.siteProfile?.title) return null;
166 168 return data;
167 169 },
168 170 async (params) => {
@@ -175,9 +177,8 @@
175 177 // provides: aiHeaders: [], aiBlogTitles: []
176 178 useRunStep(
177 179 'siteStrings',
178 180 () => {
179 - // We just need the site profile, which has this
180 181 if (!data.siteProfile?.title) return null;
181 182 return data;
182 183 },
183 184 async (params) => {
@@ -190,9 +191,8 @@
190 191 // provides: siteImages: []
191 192 useRunStep(
192 193 'siteImages',
193 194 () => {
194 - // We just need the site profile, which has this
195 195 if (!data.siteProfile?.title) return null;
196 196 return data;
197 197 },
198 198 async (params) => {
@@ -200,14 +200,27 @@
200 200 return await handleSiteImages(params);
201 201 },
202 202 );
203 203
204 + // needs: siteProfile
205 + // provides: launchDecisions: { navExtras, navButtonLabel }
206 + useRunStep(
207 + 'launchDecisions',
208 + () => {
209 + if (!data.siteProfile?.title) return null;
210 + return data;
211 + },
212 + async (params) => {
213 + checkIn({ stage: 'get_launch_decisions' });
214 + return await handleLaunchDecisions(params);
215 + },
216 + );
217 +
204 218 // needs: siteProfile, sitePlugins, siteStyle, siteImages, aiHeaders
205 219 // provides: home: { id, slug, patterns, siteStyle }
206 220 useRunStep(
207 221 'home',
208 222 () => {
209 - // Checking various data from calls above
210 223 const ok = [
211 224 data.siteProfile,
212 225 data.siteStyle,
213 226 data.siteImages,
@@ -221,14 +234,13 @@
221 234 return await handleHome(params);
222 235 },
223 236 );
224 237
225 - // siteProfile, sitePlugins, siteStyle, siteImages
238 + // needs: siteProfile, sitePlugins, siteStyle, siteImages
226 239 // provides: pages: [{ id, slug, name, patterns, siteStyle }]
227 240 useRunStep(
228 241 'pages',
229 242 () => {
230 - // Checking various data from calls above
231 243 const ok = [
232 244 data.siteProfile,
233 245 data.siteStyle,
234 246 data.siteImages,
@@ -255,14 +267,11 @@
255 267 checkIn({ stage: 'set_general', siteProfile, sitePlugins, siteStyle });
256 268
257 269 const { title } = siteProfile;
258 270 // translators: this is for a action log UI. Keep it short
259 - addStatusMessage(__('Adding admin configurations', 'extendify-local'));
260 - // update permalinks
271 + addStatusMessage(launchStrings().statusAdmin);
261 272 await updateOption('permalink_structure', '/%postname%/');
262 - // make sure consent is set
263 273 setUserGaveConsent(true);
264 - // Update title
265 274 if (title) await updateOption('blogname', title);
266 275 },
267 276 );
268 277
@@ -300,8 +309,9 @@
300 309 siteStyle,
301 310 aiBlogTitles,
302 311 siteImages,
303 312 designBuild,
313 + launchDecisions,
304 314 } = data;
305 315 // pages could be [] and pass here, that's ok
306 316 if (!home || !pages) return;
307 317 if (homeStretch.current) return;
@@ -307,10 +317,23 @@
307 317 if (homeStretch.current) return;
308 318 homeStretch.current = true;
309 319 (async () => {
310 320 const { objective, structure, category } = siteProfile;
321 + const builtHome = designBuild?.builtPages?.find((p) => p.slug === 'home');
322 + // Must match get-home.js's full-page test; if they drift, the home is
323 + // built one way and planted another.
324 + const isSinglePageDesign =
325 + structure === 'single-page' &&
326 + Boolean(builtHome?.fullPage && builtHome.patterns?.length) &&
327 + Boolean(designBuild?.pages?.length);
328 + const imageUrls = siteImageUrls(siteImages);
329 + const intendedPlugins = (sitePlugins ?? []).map(
330 + ({ wordpressSlug }) => wordpressSlug,
331 + );
311 332
312 - // Do they need an imprint page?
333 + // Guarantee plugins are active before the pattern imports below rely on them.
334 + await ensurePluginsActive(intendedPlugins);
335 +
313 336 const needsImprint = Array.isArray(showImprint)
314 337 ? showImprint.includes(wpLanguage ?? '') && category === 'Business'
315 338 : false;
316 339
@@ -319,9 +342,9 @@
319 342 let variation = siteStyle?.variation;
320 343 if (customFonts?.length) {
321 344 checkIn({ stage: 'install_fonts' });
322 345 // translators: this is for a action log UI. Keep it short
323 - addStatusMessage(__('Installing fonts locally', 'extendify-local'));
346 + addStatusMessage(launchStrings().statusFonts);
324 347 const installed = await installFontFamilies(customFonts).catch(
325 348 () => [],
326 349 );
327 350 variation = mergeFontsIntoVariation(siteStyle.variation, installed);
@@ -328,19 +351,15 @@
328 351 }
329 352
330 353 if (siteStyle?.vibe && siteStyle.vibe !== 'natural-1') {
331 354 // translators: vibe in this context is a noun - the feeling of their site design.
332 - addStatusMessage(__('Setting the website style', 'extendify-local'));
355 + addStatusMessage(launchStrings().statusSiteStyle);
333 356 checkIn({ stage: 'compute_vibe' });
334 - const vibeBlocks = await computeVibeAdjustedBlocks(
357 + const vibe = await computeVibeAdjustments(
335 358 siteStyle.vibe,
359 + variation,
336 360 ).catch(() => null);
337 - if (vibeBlocks) {
338 - variation = {
339 - ...variation,
340 - styles: { ...variation.styles, blocks: vibeBlocks },
341 - };
342 - }
361 + if (vibe) variation = { ...variation, ...vibe };
343 362 }
344 363
345 364 checkIn({ stage: 'set_vibe' });
346 365 await updateVariation(variation);
@@ -345,9 +364,9 @@
345 364 checkIn({ stage: 'set_vibe' });
346 365 await updateVariation(variation);
347 366
348 367 // navigation menu
349 - addStatusMessage(__('Working on the navigation', 'extendify-local'));
368 + addStatusMessage(launchStrings().statusNavigation);
350 369 const { id: headerNavId } = await createNavigation({
351 370 title: __('Header Navigation', 'extendify-local'),
352 371 slug: 'site-navigation',
353 372 });
@@ -353,12 +372,13 @@
353 372 });
354 373 let headerCode = updateNavAttributes(home.headerCode || '', {
355 374 ref: headerNavId,
356 375 });
376 + headerCode = injectNavExtras(headerCode, launchDecisions);
357 377 // remove the header navigation from the landing page
358 378 if (objective === 'landing-page') {
359 379 // translators: this is for a action log UI. Keep it short
360 - addStatusMessage(__('Perfecting a landing page', 'extendify-local'));
380 + addStatusMessage(launchStrings().statusLanding);
361 381 const social =
362 382 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis;
363 383 headerCode = headerCode
364 384 .replace(/<!--\s*wp:navigation\b[^>]*.*\/-->/gis, '')
@@ -363,23 +383,27 @@
363 383 headerCode = headerCode
364 384 .replace(/<!--\s*wp:navigation\b[^>]*.*\/-->/gis, '')
365 385 .replace(social, '');
366 386 }
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 - }
387 + headerCode = headerCode.replaceAll(
388 + '206-555-0100',
389 + (typeof siteProfile.phoneNumber === 'string' &&
390 + siteProfile.phoneNumber) ||
391 + // 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.
392 + __('206-555-0100', 'extendify-local'),
393 + );
376 394 checkIn({ stage: 'set_navigation' });
377 - await updateTemplatePart('extendable/header', headerCode);
378 395
379 396 // footer
380 397 let footerNavId = null;
381 398 let footerCode = home.footerCode || '';
399 + // The logo already carries the brand, so the site title would be redundant.
400 + if (designBuild?.hasExternalLogo && footerCode.includes('wp:site-logo')) {
401 + footerCode = footerCode.replace(
402 + /\s*<!--\s*wp:site-title[\s\S]*?\/-->/g,
403 + '',
404 + );
405 + }
382 406 if (needsImprint) {
383 407 const nav = await createNavigation({
384 408 title: __('Footer Navigation', 'extendify-local'),
385 409 slug: 'footer-navigation',
@@ -386,14 +410,15 @@
386 410 });
387 411 footerNavId = nav.id;
388 412 footerCode = updateNavAttributes(footerCode, { ref: footerNavId });
389 413 }
414 + footerCode = applySocialProfiles(footerCode, siteProfile.socialProfiles);
390 415 checkIn({ stage: 'set_footer' });
391 416 await updateTemplatePart('extendable/footer', footerCode);
392 417
393 418 // pages
394 419 // translators: this is for a action log UI. Keep it short
395 - addStatusMessage(__('Creating pages', 'extendify-local'));
420 + addStatusMessage(launchStrings().statusCreatingPages);
396 421 const pagesToCreate = getPagesToCreate(data);
397 422 const titlePattern = pages?.[0]?.patterns?.find((p) =>
398 423 p.patternTypes?.includes('page-title'),
399 424 );
@@ -402,9 +427,9 @@
402 427 await updatePageTitlePattern(titlePattern.code);
403 428 }
404 429
405 430 const activePlugins = await getActivePlugins();
406 - // This lets us keep plugin pages in th enav but skip making the page
431 + // Keep plugin pages in the nav but skip creating the page itself.
407 432 const reservedSlugs = new Set(
408 433 PLUGIN_OWNED_PAGES.filter(
409 434 ({ plugin }) =>
410 435 sitePlugins.some((p) => p.wordpressSlug === plugin) ||
@@ -428,25 +453,43 @@
428 453 }
429 454 checkIn({ stage: 'generate_page_content' });
430 455 const customPages = await generatePageContent(pagesReplaced, data);
431 456
432 - const stickyNav =
433 - structure === 'single-page' && objective !== 'landing-page';
434 - const createdPagesWP = await createWpPages(customPages, { stickyNav });
457 + // Update heroDescription to the actual AI-rewritten hero content
458 + const homePage = customPages.find((p) => p.slug === 'home');
459 + const heroPattern = homePage?.patterns?.find((p) =>
460 + p.patternTypes?.includes('hero-header'),
461 + );
462 + const pMatch = heroPattern?.code?.match(/<p[^>]*>([\s\S]*?)<\/p>/);
463 + const heroDesc = pMatch?.[1]?.replace(/<[^>]+>/g, '').trim();
464 + const heroDescription = heroDesc || data.heroDescription;
465 + setData('heroDescription', heroDescription);
466 + if (heroDescription) {
467 + await updateOption('extendify_hero_description', heroDescription).catch(
468 + () => null,
469 + );
470 + }
471 +
472 + const createdPagesWP = await createWpPages(customPages, {
473 + skipSectionIds: isSinglePageDesign,
474 + });
435 475 // Aux pages
436 - const hasBlogPattern = home?.patterns?.some((pattern) =>
476 + const blogPattern = home?.patterns?.find((pattern) =>
437 477 pattern.patternTypes.includes('blog-section'),
438 478 );
439 - if (objective === 'blog' || hasBlogPattern) {
479 + if (siteProfile.blog || blogPattern) {
440 480 checkIn({ stage: 'create_blog_sample_data' });
441 481 // translators: this is for a action log UI. Keep it short
442 - addStatusMessage(__('Creating blog sample data', 'extendify-local'));
443 - await createBlogSampleData({ aiBlogTitles }, siteImages);
482 + addStatusMessage(launchStrings().statusBlog);
483 + await createBlogSampleData(
484 + { aiBlogTitles },
485 + imageUrls,
486 + blogPattern?.blogImages,
487 + );
444 488 }
445 - // If we have site images then set up the hello world image
446 - if (siteImages?.length) {
489 + if (imageUrls.length) {
447 490 checkIn({ stage: 'set_hello_world_image' });
448 - await setHelloWorldFeaturedImage(siteImages);
491 + await setHelloWorldFeaturedImage(imageUrls);
449 492 }
450 493
451 494 let imprint = {};
452 495 if (needsImprint) {
@@ -458,9 +501,9 @@
458 501 if (alreadyActive(activePlugins, 'woocommerce')) {
459 502 checkIn({ stage: 'import_woocommerce_products' });
460 503 addStatusMessage(
461 504 // translators: this is for a action log UI. Keep it short
462 - __('Setting up your online store', 'extendify-local'),
505 + launchStrings().statusStore,
463 506 );
464 507 await apiFetchWithTimeout({
465 508 path: '/extendify/v1/auto-launch/import-woocommerce',
466 509 }).catch(() => null);
@@ -475,18 +518,36 @@
475 518 link: `${homeUrl}/events`,
476 519 });
477 520 }
478 521
522 + // The design's page list omits the posts page like it omits shop, so the
523 + // nav needs it here — but it's ours to create, not a PLUGIN_OWNED_PAGES.
524 + pluginPages.push(...createdPagesWP.filter(isBlogPage));
525 +
479 526 // Adding pages to the nav
480 527 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);
528 + let linksResult = { wpPages: createdPagesWP, headerCode };
529 + if (!isSinglePageDesign) {
530 + linksResult =
531 + structure === 'single-page'
532 + ? await updateSinglePageLinksToSections(
533 + createdPagesWP,
534 + customPages,
535 + {
536 + objective,
537 + activePlugins,
538 + landingPageCTALink: siteProfile.landingPageCTALink,
539 + },
540 + headerCode,
541 + )
542 + : await updateButtonLinks(createdPagesWP, pluginPages, headerCode);
543 + }
544 + const pagesWithLinksUpdated = linksResult.wpPages;
545 + headerCode = linksResult.headerCode;
546 + if (alreadyActive(activePlugins, 'woocommerce')) {
547 + headerCode = injectWooCommerceIcons(headerCode);
548 + }
549 + await updateTemplatePart('extendable/header', headerCode);
489 550 const footerNavPages = [];
490 551 if (footerNavId && imprint?.title) {
491 552 const { originalSlug, title } = imprint;
492 553 footerNavPages.push({
@@ -499,9 +560,20 @@
499 560
500 561 checkIn({ stage: 'set_navigation_links' });
501 562 if (objective !== 'landing-page') {
502 563 const orderedSlugs = designBuild?.pages?.map((p) => p.slug) ?? [];
503 - if (structure === 'single-page') {
564 + // The tag only exists where the sections came back matched 1:1 to our list;
565 + // read it off what was planted, so menu and section ids can't disagree.
566 + const designOwnsNav =
567 + isSinglePageDesign ||
568 + Boolean(homePage?.patterns?.some(({ navSlug }) => navSlug));
569 + if (designOwnsNav) {
570 + await addSectionLinksFromDesign(
571 + headerNavId,
572 + designBuild.pages,
573 + pluginPages,
574 + );
575 + } else if (structure === 'single-page') {
504 576 await addSectionLinksToNav(
505 577 headerNavId,
506 578 home?.patterns,
507 579 pluginPages,
@@ -533,11 +605,18 @@
533 605 await prefetchAssistData();
534 606 checkIn({ stage: 'final_steps' });
535 607 await setThemeRenderingMode('template-locked');
536 608 await postLaunchFunctions();
609 + if (imageUrls.length) {
610 + await storeSiteImages(siteImages).catch(() => null);
611 + }
537 612 // 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 });
613 + addStatusMessage(launchStrings().statusDone);
614 + await Promise.all([
615 + reportInactivePlugins(intendedPlugins).catch(() => null),
616 + reportFailedDependencies(pagesReplaced),
617 + checkIn({ stage: 'finished', siteProfile, sitePlugins, siteStyle }),
618 + ]);
540 619 setWarnOnReload(false);
541 620 setDone(true);
542 621 })().catch((error) => {
543 622 console.error(error);
@@ -547,14 +626,9 @@
547 626 });
548 627 // if we error here we can try again by resetting the home stretch and stalling again to refetch data
549 628 homeStretch.current = false;
550 629 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 - );
630 + setErrorMessage(launchStrings().errorFinalSteps);
557 631 });
558 632 }, [data, needToStall, setUserGaveConsent]);
559 633
560 634 return { done };
@@ -578,12 +652,7 @@
578 652 useEffect(() => {
579 653 if (!error || needToStall()) return;
580 654 console.error(error);
581 655 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 - );
656 + setErrorMessage(launchStrings().errorStep);
588 657 }, [error, setErrorMessage, needToStall]);
589 658 };