PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/AutoLaunch/hooks/useCreateSite.js +158 -89 3.0.5 → 3.2.2 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 };